Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Summary

FFI-compatible and calling-convention-compatible complex types are to be introduced into core to ensure synchronity with C primitives.

Motivation

The C standard defines the memory layout of a complex number, but not their calling convention. This means crates like num-complex require workarounds to interface with FFI using _Complex, and cannot pass values directly. The addition of complex numbers to Rust as a lang-item ensures a correct calling convention consistent with C on all platforms, thus better allowing C interop.

In essence, this RFC makes code like this:

extern double _Complex computes_function(double _Complex x);

callable in Rust without indirection:

extern "C" {
  fn computes_function(x: Complex<f64>) -> Complex<f64>;
}
fn main() {
  let returned_value = computes_function(Complex::<f64>::new(3.0, 4.0))
}

using the standard library’s FFI-compatible complex numbers.

Guide-level explanation

  • Complex<T> numbers are in core::num and reexported in std::num, like use core::num::Complex or use std::num::Complex.
  • Complex<T> numbers can be instantiated with any component type using Complex::new(re, im) where re and im are of the same type ( includes all numbers).
let x = Complex::new(3.0, 4.0);

Simple arithmetic is supported:

let first = Complex::new(1.0, 2.0);
let second = Complex::new(3.0, 4.0);
let a = first + second; // 4 + 6i
let b = first - second; // -2 - 2i
let c = first * second; // -5 + 10i
let d = float_second / float_first; // 0.44 - 0.8i

Reference-level explanation

The core crate will provide implementations for operator traits for possible component types. They will have an internal representation similar to this (with public fields for real and imaginary parts):

// in core::num::complex, which would be a private module holding complex types
#[lang = "complex"] // for calling convention.
#[repr(C)]
#[derive(Copy, Clone, PartialEq, Debug)]
pub struct Complex<T> {pub re: T, pub im: T};

have a constructor

impl Complex<T> {
  fn new(re: T, im: T) -> Self;
}

and have simple arithmetic implementations supported:

impl<T: Add> Add for Complex<T> { type Output = Self; /* ... */ }
impl<T: Sub> Sub for Complex<T> { type Output = Self; /* ... */ }

impl Mul for Complex<T> where T: Add + Sub + Mul{ type Output = Self; /* ... */ }
impl Div for Complex<T> where Complex<T>: Div<T> { type Output = Self; /* ... */ }

Drawbacks

The multiple emitted calls to libgcc.so (__mulsc3 and the like) via compiler-builtins may cause a bit of overhead and may not be what the Rust lang team and compiler team want.

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. This is because of the undefined calling convention of complex numbers in C. For example: on powerpc64-linux-gnu, returning double _Complex doesn’t do the same thing as returning a struct with a field of type double[2]. 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.

Additionally, this provides a unified API for complex numbers. Right now, many crates define their own complex types, making interoperability complicated, even though num-complex already exports its own type. (rug::Complex being an example.) You could theoretically do something like this:

double _Complex function(double _Complex value);
void wrapper_function(double _Complex* value, double _Complex* out) {
    *out = function(*value);
}

for all functions you wish for. But this still needs to happen in C.

Alternatives:

  • Don’t do this: There are, obviously, millions of alternatives on crates.io, the foremost being num-complex. However, I believe that if we wish to support proper FFI with C, then a standard type that matches calling conventions with C complex numbers is an important feature of the language. Hence, I do not recommend this idea.
  • Use a polar layout: Polar complex numbers are undoubtedly a more optimal solution for multiplying complexes. However, I believe that if we wish to have proper FFI with C, then complex number layout should be chosen in accordance with the layout that is used in the C standard, and that is the orthogonal layout. This is also the layout used by most other languages and other crates on crates.io. Additionally, the polar form suffers from many structual issues: it is not a “natural” form for expressing complex numbers in computers - you cannot express pi exactly, so you cannot use radians for angle units. Moreover, polar complex numbers do not have a unique representation for each number - it has an infinity of zeros with all possible angles. The final problem, and in my opinion the most fatal, is the complexity of addition:

$\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(atan2({r_1\sin\theta_1+r_2\sin\theta_2},{r_1\cos\theta_1+r_2\cos\theta_2}))$

which offsets any benefits that multiplication may bring.

  • Non-generic primitive types: These are, obviously, the most obvious and practical solution. However, if we implemented lots of such types, then we would not be able to expand for f16 and f128 support without repeating the code already implemented. It would be extremely repetitive and tedious to document new types and their behavior, even if we used macros to generate implementations.
  • Only in std::ffi: Many suggestions have been given that Complex remains a type in only std::ffi. However, these miss a key point of the RFC: this addition is also about creating a unified interface for complex number support in std itself, and making it an FFI type would go against that.

Prior art

FORTRAN, C, C++, Go, Perl and Python all have complex types implemented in the standard library or as a primitive. This clearly appears to be an important feature many languages have. For example, in Python:

complex_num = 1 + 2j
complex_second = 3 + 4j
print(complex_num * complex_second)

or in C:

float _Complex cmplx = 1 + 2*I;
float _Complex two_cmplx = 3 + 4*I;
printf("%.1f%+.1fi\n", creal(cmplx * two_cmplx), cimag(cmplx * two_cmplx));

Even in Rust, it has been discussed two times in IRLO:

Many crates, like num-complex also provide this feature, though it is not FFI-safe.

Unresolved questions

Future possibilities

  • Maybe later on, we can think of adding a special custom suffix for complex numbers (1+2j for example), and using that as a simpler way of writing complex numbers if this RFC is accepted? This is very similar to how most languages implement complex numbers? Or perhaps we could consider a constant:
impl<T: Float> Complex<T: Float> {
  const I: T = Complex::new(T::zero(), T::one());
}

where zero and one is implemented on the Float trait similar to num_traits? Or maybe we could have a method on normal numbers:

// for example
impl f32 {
  fn i(self) -> Complex<f32> {
    Complex::new(0, self)
  }
}

that could help simplify the life of people who otherwise would have to keep writing Complex::new()?

  • Arithmetic operations for primitives with complexes? (E.g. 1+Complex::new(0, 2)). This goes hand in hand with the previous suggestion, so if we choose to implement this we should implement it with the previous suggestion.
  • Should we support Imaginary eventually? This RFC doesn’t cover it, but I think we can do this later in another RFC.
  • Eventually we may support Gaussian integers (an extension of the real integers) which have a Euclidean division procedure with remainder. GCC has these, and we could theoretically eventually support these integers alongside GCC FFI.
  • We can also support f16 and f128 once methods for them are stabilised.
  • We should also think about a Display implementation. Should we support something like 1 + 2i or something else? Should we not make a Display impl at all, and just use re() and im() for the implementation?
  • We should also consider adding aliases (like c32 and c64) for floating points once they are established, to allow for a shorthand syntax.
  • Eventually, we should also consider adding polar conversions (e.g, modulus and angle).
  • And also, we should consider adding complex trig functions (csin, ccos, etc.) that were deliberately left out of the MVP.
  • Compatibility with other core::num types (NonZero, Saturating, Wrapping).