Skip to content

Add core::ffi::c_longdouble - #4003

Open
folkertdev wants to merge 11 commits into
rust-lang:masterfrom
folkertdev:c-longdouble
Open

Add core::ffi::c_longdouble#4003
folkertdev wants to merge 11 commits into
rust-lang:masterfrom
folkertdev:c-longdouble

Conversation

@folkertdev

@folkertdev folkertdev commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

View all comments

Add core::ffi::c_longdouble, to match C long double, and its prerequisites.

use core::ffi::c_longdouble;

extern "C" fn f(x: c_longdouble) -> c_longdouble {
    x
}

This type is highly platform-specific, A long double can be:

  • f32, e.g. on AVR
  • f64, e.g. on many 32-bit targets and some 64-bit target using MUSL
  • f128, e.g. on aarch64 and riscv

So far so good, but on some platforms this type corresponds to a type that Rust cannot currently express:

  • X87 f80 on x86 and x86_64, an IEEE (ish) 80-bit floating point type
  • IBM f128 on powerpc and powerpc64, a "double double"

This RFC proposes the addition of these two types, with extremely minimal APIs, so that a portable core::ffi::c_longdouble can be defined.

Thanks to @tgross35, @beetrees and @Gelbpunkt for early review on this RFC.

Tracking issue:

Important

Since RFCs involve many conversations at once that can be difficult to follow, please use review comment threads on the text changes instead of direct comments on the RFC.

If you don't have a particular section of the RFC to comment on, you can click on the "Comment on this file" button on the top-right corner of the diff, to the right of the "Viewed" checkbox. This will create a separate thread even if others have commented on the file too.

Rendered

@folkertdev folkertdev added the T-lang Relevant to the language team, which will review and decide on the RFC. label Aug 20, 2026
Comment thread text/0000-c_longdouble.md
Comment on lines +318 to +324
**X87 F80**

- `f80x87`, consistent with the recent `f16b`
- `f80`, rejected because it suggests this is a first-class type like f32 and f128, it is not
- `__float80`, similar to `__m256i` and similar platform-specific types in stdarch
- `x86f80` or `x86_f80` or `X86F80`
- `x87f80` or `x87_f80` or `X87F80`

@folkertdev folkertdev Aug 20, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

What should the X87 F80 type be called?

To keep this somewhat organized it would help if you could start your post with a list

  1. of your most preferred option
  2. to
  3. your least preferred but still acceptable option

That way we can get a sense of what names have some consensus, even if they are not everyone's favorite. You can then of course elaborate on your reasoning.

View changes since the review

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

since these are supposed to live in core::arch::x86::* not core::num::* (line 46), the name should follow whatever naming convention used in the platform intrinsics (which is why we retain the ugly names like __m128 and _mm_xor_pd).

i think msvc and icc don't give it as special name, just long double. so core::arch::x86::__float80 following GCC & clang is perhaps the only good answer.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

For me, I'd say:

  1. x87_f80
  2. f80_x87
  3. __float80
  4. f80x87

f80x87 looks too similar to vector types like f32x16. It's nice to have x87 indicating what it is, __float80 is nice for consistency with the C compilers but doesn't do that quite as well.

x87_f80 reads slightly better than f80_x87 because it feels more like a scoping that is extensible. Not that we're likely to actually extend it, but x87_fsqrt, x87_fsin, x87_fptan strike me as better names for operations than fsqrt_x87.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

It's nice to have x87 indicating what it is, __float80 is nice for consistency with the C compilers but doesn't do that quite as well.

TBF if ambiguity is an issue, using the qualified form like core::arch::{x86, x86_64}::__float80 is clear enough for distinguishing from e.g. a core::arch::m68k::__float80.

(BTW the C23 standard named the type _Float64x (extended-precision from _Float64), which I find it a very bad idea)

@RalfJung RalfJung Aug 26, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

For everything else in core::arch we use exactly the C name, so I think we should do the same here:

  1. core::arch::{x86, x86_64}::__float80
  2. (big gap)
  3. core::arch::{x86, x86_64}::x87_f80 / core::arch::{x86, x86_64}::f80_x87

Comment thread text/0000-c_longdouble.md
Comment on lines +326 to +333
**IBM F128**

- `f128ppc`, consistent with the recent `f16b`
- `__ibm128`, similar to `__m256i` and similar platform-specific types in stdarch
- `ppcf128` or `ppc_f128`
- `ibmf128` or `ibm_f128`
- `doubledouble` or `DoubleDouble`
- `f64f64` or `F64F64`

@folkertdev folkertdev Aug 20, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

What should the IBM F128 type be called?

To keep this somewhat organized it would help if you could start your post with a list

  1. of your most preferred option
  2. to
  3. your least preferred but still acceptable option

That way we can get a sense of what names have some consensus, even if they are not everyone's favorite. You can then of course elaborate on your reasoning.

View changes since the review

@Gelbpunkt Gelbpunkt Aug 25, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

I find these names acceptable, in order of preference:

  1. ibmf128 or ibm_f128
  2. __ibm128, but to be in line with the newer stdarch types, I'd prefer omitting the __, so ibm128
  3. doubledouble or DoubleDouble

Reason for 1. and 2. is that they are very much in line with the C type names and are not ambiguous or misleading. I consider 3. also okay, and it is the more descriptive name for the type if we care about not naming a type after IBM.

I would like to avoid f128ppc or ppcf128/ppc_f128 et al. because to me these seem like they would imply that this is the "f128 for PowerPC", which it really isn't. There's both the IEEE quad precision f128 on PowerPC as well as the IBM double-double type, and they're distinct things. f128 in Rust is the IEEE quad precision type, and that one exists under the same name.

For f64f64 I just think it's an all-around worse version of doubledouble naming-wise. This would be the name I would perhaps choose if this were some kind of type for more than just FFI purposes, but given the context, doubledouble makes more sense to me.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

In my opinion:

  1. ibm128
  2. __ibm128
  3. DoubleDouble

Given the primary purpose of the types is C interop, I think matching the C name (with or without the leading underscores) is the best choice. I don't think we should use anything containing f128 as that gives the misleading impression that this is related to the actual f128 type, which it is not. If we don't want to go with the C name, I think DoubleDouble is the best choice as according to this paper the type is "called “double-double” in most of the literature". If we don't aim to match the C type name with ibm128/__ibm128, I think we should stick with the Rust standard CamelCase naming convention as this is not a Rust primitive type.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

IBM surely is a registered trademark, do we have to be concerned about using it in a type name?

@RalfJung RalfJung Aug 26, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Assuming that's not an issue, I think my default choice would be that, as usual in core::arch, we should use exactly the C name:

  1. core::arch::{powerpc, powerpc64}::__ibm128
  2. core::arch::{powerpc, powerpc64}::ibm128
  3. (big gap)
  4. core::arch::{powerpc, powerpc64}::F64Pair or so

None of the "new" names really cut it for me. I don't think we should start using "double" to refer to f64 when that is not a Rust term and not even C does this here. So the only alternatives that appeal to me involve indicating that it's a pair of f64.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

IBM surely is a registered trademark, do we have to be concerned about using it in a type name?

I wouldn't expect that to be an issue since we're using IBM to refer to a type they invented and they chose that name, sorta like it's just fine to use std::os::windows to refer to a trademarked OS that Microsoft invented.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

since when is registered trademark a problem? "Arm", "MIPS", "AMD", "Hexagon", "RISC-V", "Windows", "UNIX", "Linux" are all registered trademark and they all appear in core::arch::* or std::os::*.

(if using __ibm128 violates the IBM trademark rules, the existing prior art in GCC and LLVM must also be considered 🙃)

@RalfJung RalfJung Aug 26, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Those are all good points regarding trademarks, sorry for the distraction.

Comment thread text/0000-c_longdouble.md
Comment on lines +140 to +144
### Normalization

Operations on `f128ppc` assume that the value is normalized: the high component is assumed to be at least as large was the low component. Hence whether a value is positive or negative can only look at the sign bit of the high component.

When creating a `f128ppc` value from bytes (using `transmute`, `from_le_bytes`, etc) this invariant can be broken. That can cause nonsensical results, but cannot cause UB.

@beetrees beetrees Aug 20, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Relevant wording from the ABI specs. The summary is that high + low (the regular f64 add operation) must equal high, and that we should always set the low to +0/-0 on infinity (the ELFv1 spec, at least the latest version I could find, doesn't seem to require this so we should make sure we can accept infinities with other values of low).

64-bit ELFv2

In this format, double-precision numbers with different magnitudes that do not overlap provide an effective precision of 106 bits or more, depending on the value. The high-order double-precision value (the one that comes first in storage) must have the larger magnitude. The high-order double- precision value must equal the sum of the two values, rounded to nearest double (the Linux convention, unlike AIX).

(In the ELFv2 spec, the following quote appears in the previous section which is about the standard IEEE f128; however this appears to be a drafting error as it refers to the low-order value being positive or negative zero, which only makes sense in the context of IBM f128, not IEEE f128. Additionally, a similar sentence appears in the correct section in the ELFv1 spec.)

This implementation does not fully support the IEEE special numbers NaN and INF. These values are encoded in the high-order double value only. The low-order value is not significant, but the low-order value of an infinity must be positive or negative zero.

64-bit ELFv1

"Extended precision" is the IBM AIX® 128-bit long double format composed of two double-precision numbers with different magnitudes that do not overlap. The high-order double-precision value (the one that comes first in storage) must have the larger magnitude. The value of the extended-precision number is the sum of the two double-precision values.

Does not fully support the IEEE special numbers NaN and INF. These values are encoded in the high-order double value only. The low-order value is not significant.

32-bit

In this format, double precision numbers with different magnitudes that do not overlap, provide an effective precision of 106-bits. The high-order double-precision value (the one that comes first in storage) must have the larger magnitude. The high-order double-precision value must equal the sum of the two values, rounded to nearest double.

Does not fully support the IEEE special numbers NaN and INF. These values are encoded in the high-order double value only. The low-order value is not significant, but the low-order value of an infinity must be positive or negative zero.

IBM AIX documentation

A 128-bit long double number consists of an ordered pair of 64-bit double-precision numbers. The first member of the ordered pair contains the high-order part of the number, and the second member contains the low-order part. The value of the long double quantity is the sum of the two 64-bit numbers.

Each of the two 64-bit numbers is itself a double-precision floating-point number with a sign, exponent, and significand. The low-order member has a magnitude that is less than 1 unit in the last place of the high part, so the values of the two 64-bit numbers do not overlap and the entire significand of the low-order number adds precision beyond the high-order number.

Comment thread text/0000-c_longdouble.md

Operations on `f128ppc` assume that the value is normalized: the high component is assumed to be at least as large was the low component. Hence whether a value is positive or negative can only look at the sign bit of the high component.

When creating a `f128ppc` value from bytes (using `transmute`, `from_le_bytes`, etc) this invariant can be broken. That can cause nonsensical results, but cannot cause UB.

@RalfJung RalfJung Aug 20, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
When creating a `f128ppc` value from bytes (using `transmute`, `from_le_bytes`, etc) this invariant can be broken. That can cause nonsensical results, but cannot cause UB.
When creating a `f128ppc` value from bytes (using `transmute`, `from_le_bytes`, etc) this invariant can be broken. That can cause nonsensical results, but cannot cause UB in Rust: Rust code must never rely on this invariant for safety.
However, C code may rely on this invariant, so care must be taken when passing Rust-produced `f128ppc` values to C code.

This does sound like a major footgun...

View changes since the review

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Having given this some thought, I think ideally we probably want to match C here (even if it would create safety invariants) so that callers of C APIs (one of the main use cases of this type) don't have to check a f128ppc is valid before every call, however I'm not 100% sure whether it's intended to be UB in C or not. Clang/LLVM will currently optimise __ibm128 ops assuming that the value is normalised (making it UB for it not to be), however I'm not sure if that's intentional or a bug caused by generic float optimisations that were written only thinking about standard IEEE floats - I haven't found anything in the LLVM LangRef saying one way or the other. However, the PowerPC ABI specifications use the word "must" when describing the layout of the IBM long double, which does suggest it is in fact a safety requirement, just a poorly documented one.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

So something like this?

impl f128ppc {
    pub unsafe fn from_components(large: f64, small: f64) -> Self {
        core::mem::transmute([large, small])
    }

    fn is_normal_form(large: f64, small: f64) {
        if large.is_nan() {
            true
        } else if large.is_infinite() {
            // This branch may need to be skipped on elfv1 and aix.
            small == 0.0
        } else {
            large + small == large
        }
    }

    pub fn checked_from_components(large: f64, small: f64) -> Option<Self> {
        if Self::is_normal_form(large, small) {
            Some(unsafe { Self::from_components(large, small) })
        } else { 
            None
        }
    }
}

cc the IBM target maintainers @daltenty @amy-kwan @lei137, can you add the safety requirements to the LLVM langref?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Also on the LLVM side it would be good to know if this is a precondition for all float operations, or an assumption on all values of the type. If I just store such a value to memory and look at the bits, will LLVM assume this invariant?

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

So something like this?

Yes. Another possibility is (instead of/as well as):

    pub fn normalize_from_components(x: f64, y: f64) -> Self {
        if Self::is_normal_form(x, y) {
            unsafe { Self::from_components(x, y) }
        } else {
            let large = x + y;
            if !large.is_finite() {
                return unsafe { Self::from_components(large, 0.0) };
            }
            // Per https://doi.org/10.1145/3121432, Algorithm 2
            let x1 = large - y;
            let y1 = large - x1;
            let x2 = x - x1;
            let y2 = y - y1;
            let small = x2 + y2;
            debug_assert!(Self::is_normal_form(large, small));
            unsafe { Self::from_components(large, small) }
        }
    }

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I've also found https://github.com/bminor/glibc/blob/master/sysdeps/ieee754/ldbl-128ibm/s_iscanonicall.c, which is a bit different (e.g. does not distinguish elfv2 from the rest).

Overall I'm having some trouble with what (1.0, inf) should normalize to. The implementation I posted earlier says (1.0, 0.0) but that seems wrong? But then what about (inf, -inf), that would return NaN when the components are just added.

@beetrees beetrees Aug 30, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

I suspect the ELFv1 specification saying that "The low-order value is not significant." for infinity is a specification bug as it conflicts with "The value of the extended-precision number is the sum of the two double-precision values." for (inf, -inf) as you've noticed.

Generally I'd interpret normalizing as adding the two components together and giving the result as a f128ppc (which is always an exact operation, and will always result in the same value modulo NaN bits(/zero signs?) etc. for numbers which are already normalized), so (1.0, inf) would normalize to (inf, 0.0) and (inf, -inf) would normalize to (nan, 0.0).

Comment thread text/0000-c_longdouble.md
# Motivation
[motivation]: #motivation

A systems programming language should be able to express any signature that C can express. Rust already provides many ABI-compatible types in `core::ffi::*`, and the recent stabilization of c-variadic functions and the `VaList` type is another step in this direction. But, holes remain, and they should be plugged.

@RalfJung RalfJung Aug 20, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Is there a more concrete argument than this? Like, some actual API that needs these types where someone may want to use Rust?

Personally I agree that it is desirable for Rust to be able to express any signature C can express, but there's a limit for how far I'd be willing to go to achieve that property.

View changes since the review

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

one API I can think of is when implementing a libc in Rust, you need to be able to use long double in printf, strtold, and a bunch of other functions that C expects to work.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

e.g. https://github.com/sunfishcode/c-ward which uses https://github.com/lights0123/printf-compat which can't implement printf for long double since that has no corresponding rust type.

Comment thread text/0000-c_longdouble.md Outdated
Comment thread text/0000-c_longdouble.md Outdated
Co-authored-by: Juniper Tyree <50025784+juntyr@users.noreply.github.com>
@folkertdev folkertdev added the I-lang-nominated Indicates that an issue has been nominated for prioritizing at the next lang team meeting. label Aug 22, 2026
Comment thread text/0000-c_longdouble.md Outdated
Comment thread text/0000-c_longdouble.md Outdated
@traviscross traviscross added I-lang-radar Items that are on lang's radar and will need eventual work or consideration. P-lang-drag-1 Lang team prioritization drag level 1. labels Aug 25, 2026
@traviscross

Copy link
Copy Markdown
Contributor

While on lang we review the RFC, I'm happy to champion a lang experiment for this, @folkertdev, if you want to create the tracking issue.

@traviscross traviscross changed the title add core::ffi::c_longdouble Add core::ffi::c_longdouble Aug 25, 2026
Comment thread text/0000-c_longdouble.md Outdated
Co-authored-by: kennytm <kennytm@gmail.com>
@RalfJung

Copy link
Copy Markdown
Member

@Gelbpunkt

but to be in line with the newer stdarch types, I'd prefer omitting the __, so ibm128

Has stdarch started omitting the __? I wasn't aware of that. How many cases of this are there, and are there any recent counterexamples?

@Gelbpunkt

Gelbpunkt commented Aug 26, 2026

Copy link
Copy Markdown

@Gelbpunkt

but to be in line with the newer stdarch types, I'd prefer omitting the __, so ibm128

Has stdarch started omitting the __? I wasn't aware of that. How many cases of this are there, and are there any recent counterexamples?

It is omitted for RISC-V and LoongArch intrinsics and types. See also this thread and onwards when this came up for LoongArch intrinsics. Since the PowerPC vector types and intrinsics don't have an __ prefix in Rust or C, I think it's fine to omit it here for the ibm128 (or choose your flavor of name) as well, it wouldn't cause inconsistency with existing intrinsics.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

I-lang-nominated Indicates that an issue has been nominated for prioritizing at the next lang team meeting. I-lang-radar Items that are on lang's radar and will need eventual work or consideration. P-lang-drag-1 Lang team prioritization drag level 1. T-lang Relevant to the language team, which will review and decide on the RFC.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

9 participants