For FFI purposes, is it valid to have a tuple in a `repr(C)` struct?

For example, consider a C struct defined like this:

struct S1 {
    uint8_t a;
    uint32_t b;
    uint32_t c;
};

Ordinarily I would translate this to a Rust struct like so:

#[repr(C)]
struct S2 {
    a: u8,
    b: u32,
    c: u32,
}

I'm wondering if it would be safe to have this instead:

#[repr(C)]
struct S3 {
    a: u8,
    t: (u32, u32),
}

Is S3 guaranteed to have the same layout as S1/S2, and can I safely pass it to a C function?

The nomicon section on repr(C) has a couple relevant bullet points:

  • DST pointers (wide pointers) and tuples are not a concept in C, and as such are never FFI-safe.
  • Tuple structs are like structs with regards to repr(C), as the only difference from a struct is that the fields aren’t named.

Neither bullet point seems like a 100% clear answer for this case to me.

(Background: I don't actually particularly want to do this, but I'm looking at some code that already does it and wondering if it's OK.)

Note that when it says "tuple struct" that refers to user-defined tuple-structs, not tuples in general. The answer is no, this is not safe to rely on the layout of tuples.

No, that's not guaranteed and it's not safe to pass it to a C function.

This being said it's implementation-defined behavior, not undefined behavior which means that you can add appropriate test to ensure that code works when you need to use third-party code.

First point answers it perfectly: tuples are not a concept in C, and as such are never FFI-safe. I don't see what kind of misunderstanding can there be.

Second is irrelevant: tuple structs and tuples are different things.

Thanks for your answers.

My uncertainty comes from not understanding why a tuple struct would be OK, but a struct containing a tuple isn't. As the nomicon says, "Tuple structs are like structs with regards to repr(C), as the only difference from a struct is that the fields aren’t named." It seems like that logic would hold just fine for a tuple embedded in a struct as well. (In contrast, a bare tuple not inside a struct wouldn't have repr(C), so I can see why that would be disallowed.)

That one is easy:

#[repr(C)]
struct Foo(i32, i32);

struct Bar(i32, i32);

Here Foo is FFI-safe, while Bar is not FFI-safe. That is what nomicon is talking about. You can not add #[repr(C)] to tuple thus it's never FFI-safe.

It's the opposite: if you use non-FFI safe type like tuple or normal (not #[repr(C)]) struct in a defintion of your your #[repr(C)] struct then the whole combo is not FFI-safe.

It's simply impractical. If we would assume that your idea works then the whole notion of separate compilation, local reasoning and everything else flies out of the window.

Imagine the following code:

type FoundationalType = (i32, i32);

fn ImportantOne(ft: &mut FoundationalType) {
    …
}

… // millions lines of code, thousands of crates …

… // And then, suddenly, in some unittest:

#[test]
fn simple_test() {
    #[repr(C)]
    struct MyTestStruct {
        ft: FoundationalType
    }
    let mut mt = MyTestStruct{ft:(1, 2)};
    ImportantOne(&mut mt.ft);
}

And, suddenly, behavior of the whole million-lines thing is changed simply because someone added tiny local struct in some unittest? This idea just violates POLA principle so deeply it's not even funny.

They're making a sharp distinction between tuples (...) and tuple structs Name(...). I feel the more pertinent point is that here:

struct Tuple(u16, u64, u8);

#[repr(C)]
struct Container1 {
    field: (u16, u64, u8),
}

#[repr(C)]
struct Container2 {
    field: Tuple,
}

The layout of the the individual fields don't change just because the containers are repr(C); Container1::field has the layout of a tuple (i.e. repr(Rust) with few guarantees), and Container2::fields has the layout of Tuple.

The layout of Tuple is also repr(Rust) by default, so you actually want:

+#[repr(C)]
 struct Tuple(u16, u64, u8);

And there's no way to add #[repr(C)] to tuples (...) as it's a foreign type from the POV of your code. Therefore there's no way to put a tuple (...) in your container and have everything like size and alignment and offsets stably defined.

The same is true for any other foreign non-repr(C) struct, unless you convince the owner to add that attribute (which isn't going to happen with most builtin types, where the flexibility for the compiler is intentional).


A repr(C) struct containing a non-repr(C) field may still be somewhat usable if, for example, you pass a pointer, and the non-repr(C) fields are at the end, and the foreign code only reads the leading repr(C) fields, or some-such. But that's a pretty niche consideration and not what you want:

Note that there are an interesting corner-case where such use is allowed. But that's very narrow exception to handle nullable pointers on the C side. References, function pointers, NonNull and Option, basically. These are way too common and thus have special guarantees.

But tuples and other such things… no.

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.