Skip to content

Conversation

@SciMind2460
Copy link

@SciMind2460 SciMind2460 commented Dec 2, 2025

This RFC proposes FFI-compatible complex numbers to help scientific computing library authors use non-indirected complexes.

I apologise in advance to num-complex

Rendered

@ehuss ehuss added the T-libs-api Relevant to the library API team, which will review and decide on the RFC. label Dec 2, 2025
@joshtriplett joshtriplett added the T-lang Relevant to the language team, which will review and decide on the RFC. label Dec 2, 2025
@joshtriplett
Copy link
Member

Labeling this T-lang because the desire to make this FFI-compatible is a lang matter.

@joshtriplett joshtriplett added the I-libs-api-nominated Indicates that an issue has been nominated for prioritizing at the next libs-api team meeting. label Dec 2, 2025
@clarfonthey
Copy link

clarfonthey commented Dec 2, 2025

It's worth pointing out another big issue with this is that the canonical a+bi is not actually the best representation of complex numbers in all cases, and so deciding on this is making a decision that might make life harder for external complex-numeric libraries out there.

In particular, while a+bi (orthogonal) representation is efficient for addition, r*(iθ).exp() is more efficient for multiplication, and depending on the equation you're using, it may be advantageous to switch between the two to reduce the number of arithmetic operations needed.

I'm not super compelled by the argument that C supports this, therefore the standard library needs to support this. I think that guaranteeing a std::ffi::Complex representation would be desirable, but there's no saying that we need to make this a canonical type in, say, std::num.

@tgross35
Copy link
Contributor

tgross35 commented Dec 2, 2025

It's worth pointing out another big issue with this is that the canonical a+bi is not actually the best representation of complex numbers in all cases, and so deciding on this is making a decision that might make life harder for external complex-numeric libraries out there.

In particular, while a+bi (orthogonal) representation is efficient for addition, r*(iθ).exp() is more efficient for multiplication, and depending on the equation you're using, it may be advantageous to switch between the two to reduce the number of arithmetic operations needed.

I think that polar form almost always is the more optimal form, at least in my experience. But the ABIs do use rectangular, e.g. from x86:

Arguments of complex T where T is one of the types float or double are treated as if they are implemented as:

struct complexT {
  T real;
  T imag;
};

so it makes sense that an interchange type matches that, and users can translate to/from a polar repr at the FFI boundary if needed. But this reasoning is definitely something to have in the RFC's rationale & alternatives.

@clarfonthey
Copy link

Right: I guess my main clarification here was that due to the polar-orthogonal discrepancy, it shouldn't be a canonical Rust type (e.g. std::num::Complex shouldn't be making a decision on which is more-canonical), but I do think that having extra FF-compatibility types is reasonable and this shouldn't prevent us from adding std::ffi::Complex which is orthogonal.


The definition of complex numbers in the C99 standard defines the _memory layout_ of a complex number but not its _calling convention_.
This makes crates like `num-complex` untenable for calling C FFI functions containing complex numbers without at least a level of indirection (`*const Complex`) or the like.
Only in `std` is it possible to make an additional repr to match the calling convention that C uses across FFI boundaries.
Copy link

@SOF3 SOF3 Dec 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is FFI compatibility limited to std only? It's just like how we have all the types in crate@libc. What necessitates complex becominga type in something like crate@libc?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it's like va_list (core::ffi::va_list::VaListImpl) can only be implemented in libcore.

@SciMind2460
Copy link
Author

SciMind2460 commented Dec 3, 2025

Thanks everyone for the feedback! I have incorporated as much as I can into the RFC.
@clarfonthey I do think that the orthogonal representation is more "canonical", especially as it is the most commonly used one in crates.io and across languages. So I'm not sure if we can consider this an issue, especially as there are polar conversion methods in the RFC.

Comment on lines 167 to 174
impl Complex<f64> {
fn angle(self) {
f32::atan2(self.re(), self.im())
}
fn from_polar(modulus: f32, angle: f32) -> Complex<f32> {
Complex::new(modulus * f32::cos(angle), modulus * f32::sin(angle))
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably want to return something here? And not use the f32 types for f64

That being said: I think polar conversions should be put into "future possibilities" since they aren't needed for basic support.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was trying to write something to help with @clarfonthey's polar proposal, but if they are fine with this becoming a future possibility, then OK!

Co-authored-by: kennytm <kennytm@gmail.com>
SciMind2460 and others added 2 commits December 4, 2025 16:41
Co-authored-by: Juniper Tyree <50025784+juntyr@users.noreply.github.com>
Co-authored-by: Juniper Tyree <50025784+juntyr@users.noreply.github.com>
@SciMind2460
Copy link
Author

@clarfonthey one of the other reasons I don't agree with polar representation is that it makes it very hard to define complex integers (Gaussian integers) which I included as a future possibility and which are useful for dealing with discrete geometry.

@dlight
Copy link

dlight commented Dec 4, 2025

Right: I guess my main clarification here was that due to the polar-orthogonal discrepancy, it shouldn't be a canonical Rust type (e.g. std::num::Complex shouldn't be making a decision on which is more-canonical), but I do think that having extra FF-compatibility types is reasonable and this shouldn't prevent us from adding std::ffi::Complex which is orthogonal.

But std::num::Complex needs to have some in-memory representation in any case, and there isn't a way to make a global toggle that says "in my app, the most natural representation is polar". So the stdlib needs to pick one or another.

What the stdlib could do is to make this representation private, and reserve the right to change it without it being considered a breaking change. But why would this change ever happen? If changing the repr of this type realistically won't happen, it's useful to make this repr a public guarantee (not only it aids FFI, it also aids unsafe code, such as people writing inline asm to manipulate such a type without the need to pass through a conversion step)

@clarfonthey
Copy link

The conversion between a polar and orthogonal form isn't lossless, so, it effectively can't be done "automatically" or "as an implementation detail." You need trigonometric functions to do it, and while the conversion is algebraically closed, it's certainly more complicated to do exactly and most people prefer to just use floats instead.

My point here isn't that we need to decide; the issue is that the decision itself means that we shouldn't decide, and instead avoid having a standard Complex type for the standard library.

This doesn't preclude adding std::ffi::Complex which allows ABI-compatibility with C's _Complex, however, I don't think that such a type should be made standard for the language because of the fact that there are so many different ways to go about it.

@SciMind2460
Copy link
Author

SciMind2460 commented Dec 4, 2025 via email

@clarfonthey
Copy link

Rust doesn't have to support them, though. Gaussian integers, however useful, are not a primitive that the language needs to offer to everyone and maintain.

Comment on lines +32 to +38
## Guide-level explanation
[guide-level-explanation]: #guide-level-explanation

`Complex<T>` numbers can be instantiated as of any type using `Complex::new(re, im)` where `re` and `im` are of the same type (this includes all numbers).
```rust
let x = Complex::new(3.0, 4.0);
```
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Discussion: where should this type live?

(redirecting discussion from the main PR thread to keep things organized)

## Rationale and alternatives
[rationale-and-alternatives]: #rationale-and-alternatives

The rationale for this type is mostly FFI: C libraries that may be linked from Rust code currently cannot provide functions with direct struct implementations of Complex - they must be hidden under at least a layer of indirection. However, it is not always possible to write a C complex-valued function that wraps the first function in a pointer. Thus, FFI becomes a problem if such complex-valued functions are passed by value and not by reference.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The C spec says that T _Complex gets treated as T[2] in memory. It is my understanding that most ABIs effectively say the same. Could you give some concrete examples of platforms where this isn't the case?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

on powerpc64-linux-gnu, returning double _Complex doesn't do the same thing as returning a struct with a field of type double[2]: https://gcc.godbolt.org/z/hh7zYcnK6

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll add into the RFC

@kennytm
Copy link
Member

kennytm commented Dec 5, 2025

I guess my main clarification here was that due to the polar-orthogonal discrepancy

[...]

My point here isn't that we need to decide; the issue is that the decision itself means that we shouldn't decide, and instead avoid having a standard Complex type for the standard library.

As binary floating point cannot represent $\pi$ exactly, you can't even accurately describe $i\;(= 1\angle\tfrac\pi2)$ in polar form. In terms of representation it can work if the angle unit uses turns (multiples of $2\pi$) rather than radians. Even so, addition and subtraction under polar form is extremely complicated ($\left(r_1\angle\theta_1\right) + \left(r_2\angle\theta_2\right) = \left(\sqrt{r_1^2+r_2^2+2r_1r_2\cos(\theta_1-\theta_2)}\right)\angle\left(\tan^{-1}\tfrac{r_1\sin\theta_1+r_2\sin\theta_2}{r_1\cos\theta_1+r_2\cos\theta_2}\right)$ ) which outweighs any slight advantages brought by multiplication ($(a_1 + b_1i)\times(a_2+b_2i) = (a_1a_2 - b_1b_2) + (a_1b_2 + a_2b_1)i$ isn't really that costly in comparison), so it does not make sense as default in terms of computation either.

So I don't see how this is a valid discrepancy in the first place, no sane library will only provide a Complex<T> type in polar form, it's going to be either always rectilinear, or having multiple convertible choices StandardComplex<T>, PolarComplex<T>, EinsensteinComplex<T> etc.

If we are going to have a std::num::Complex<T>, in additional to the above reasoning, because of easy interoperability with C, C++, etc the rectilinear form is basically the only choice. That is, the "polar form" question is a total distraction, the only decision we need to make is have it, or not.

This doesn't preclude adding std::ffi::Complex which allows ABI-compatibility with C's _Complex, however, I don't think that such a type should be made standard for the language because of the fact that there are so many different ways to go about it.

Providing only core::ffi::Complex but without any associated functions is like providing core::ffi::VaList type without the arg() method, or like providing core::arch::x86::__m128 type without all the SSE _mm_* functions.

So IMO we either:

  1. Provide a Complex<T> type which is FFI-compatible with C's _Complex T at least for T = f32, f64, and exposes all methods available in C, or
  2. Declare that [T; 2] is FFI-compatible with _Complex T and be done with it. I think it is correct for Clang and GCC, but seems not the case for MSVC.

Though polar representations are more accurate, they simply wouldn't work with complex integers (Gaussian integers) which I proposed as a future possibility.

Note that Gaussian integers are not the only type of complex integers.

@programmerjake
Copy link
Member

  1. Provide a Complex<T> type which is FFI-compatible with C's _Complex T at least for T = f32, f64, and exposes all methods available in C, or
  2. Declare that [T; 2] is FFI-compatible with _Complex T and be done with it. I think it is correct for Clang and GCC, but seems not the case for MSVC.

Deciding the in-memory representation is the easy part, the hard part is how exactly complex numbers are passed by value in function arguments and return values -- I recall reading ABI specs that treat complex numbers specially such that they don't really match the ABI of any other single type (so it has to be handled specially by rustc and can't just be an existing type), though I can't currently recall which ABI specs.

@SciMind2460
Copy link
Author

SciMind2460 commented Dec 5, 2025

@programmerjake you're right - the calling convention of complex numbers is really not defined very well, which is why in my opinion No. 2 seems untenable. Sometimes complex numbers are passed as a struct, sometimes SSE registers - it really is untenable for anything other than STD to match the calling convention. (I am not an AI - I use dashes to denote pauses very frequently.)

Added theoretical C code example and alternatives for complex number handling in FFI.
Clarified rationale for Complex type regarding FFI and C calling conventions.
## Drawbacks
[drawbacks]: #drawbacks

If there is suddenly a standard-library Complex type, people may rush to include it in their current implementations, which would leave people behind if they didn't know about it. I really don't think this is a drawback though, since similar things have happened in Rust before: the inclusion of `OnceCell` in Rust, for example.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't really understand what exactly this means. Who would want to rush to include complex numbers in their code? And who would be be left behind?
I think the biggest downside here is the increased complexity and API surface and implementation maintenance.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

github put this reply in the wrong thread, afaict it was intended to be here.

@SciMind2460
Copy link
Author

SciMind2460 commented Dec 6, 2025 via email

Clarify potential drawbacks of a standard-library Complex type and its impact on implementation.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

I-libs-api-nominated Indicates that an issue has been nominated for prioritizing at the next libs-api team meeting. T-lang Relevant to the language team, which will review and decide on the RFC. T-libs-api Relevant to the library API team, which will review and decide on the RFC.

Projects

None yet

Development

Successfully merging this pull request may close these issues.