FFI Safety of Box<T>

I have the following problem in my program: There is a structure that I share between Rust and C code. It is created in Rust code and goes out of context in Rust code, so for a pointer in this struct I want to take advantage of the automatic memory management of Box. It seems to have the correct memory representation, so I just put a Box into the #[repr(C)] struct.

Lets say I share the following struct:

#[repr(C)]
struct MyStruct {
    something: u32,
    userp: Box<UserData>,
}

Now I get the following warning:

warning: extern block uses type std::boxed::Box<my::UserData>, which is not FFI-safe

at the locations where I connect to the C code:

extern "C" {
    fn do_something(var: *mut MyStruct);
}

I could of course convert the Box to a raw pointer to store it in the struct and then implement Drop, but this seems unnecessary as this works perfectly fine like this already. What would you recommend?

When a type does not have an #[repr(...)] annotation, the memory layout of that type is 100% unspecified, and could be anything. Of course, if it happens to be what you expected in this particular instance, it will work, but then you are relying on internal compiler details.

The case of box is slightly interesting, because the standard library does make a promise in the documentation, so in this case it is ok. However you should be aware that the only reason they can make this promise is that Box has been implemented by the people who wrote the compiler, so they can make promises that rely on internal compiler details, and this is only because they also control the compiler, and control thus when and how these internal compiler details change.

As for “but this seems unnecessary as this works perfectly fine like this already”, that is a very dangerous argument when you are dealing with undefined behaviour. Let me quote myself

In this case it wasn't just luck, but the there are other cases where it will just be luck, and in those cases you could have made the same argument, and therefore it is a bad argument.

I don't know why they haven't just put #[repr(transparent)] on box.

6 Likes

@alice actually, for Box and Box only, even if it is not #[repr(transparent)], Rust now guarantees it having the layout of a ptr::NonNull<T> (at least when T : Sized)

  • EDIT: Misread the comment.

This is due to the improper_ctypes lint, which is "known to be overly conservative", which has led to it being disabled for extern "C" fn definitions. In this case I guess that "the Box has the layout of ptr::NonNull" rule hasn't been integrated into the lint, hence the error.

Assuming:

  1. UserData : Sized

    • which you can ensure by adding the requirement to the struct definition:

      #[repr(C)]
      struct MyStruct
      where
          UserData : Sized,
      {
          // ...
      }
      
  2. UserData is FFI-compatible OR the C code never accesses UserData fields

    • this second case seems the most likely for an extern { function declaration: we can imagine C code having a struct { uint32_t something; void * ignored; } for implementors to use the void * as they please.

then your extern "C" function declaration is fine, and you can slap a #[allow(improper_ctypes)] on it with a comment ont top linking to this very thread or summarizing it.

Note, however, that by using Box<_> directly rather than Option<Box<_>>, you really need to ensure that C never causes the pointer to become NULL, as that would be instant UB the moment such ill-formed value gets to exist in the Rust world.

  • For instance, a do_something that "initializes" the something field of MyStruct by doing:

    #include <inttypes.h> // for uint32_t definition
    
    typedef struct {
        uint32_t something;
        void * ignored;
    } my_struct_t;
    
    void do_something (my_struct_t * var)
    {
        *var = (my_struct_t) { .something = 42 }; // this zeroes `ignored`!
    }
    

    would cause UB with your Box<UserData> definition :warning:

Also note that you are using u32 for your something field; given the do_something declaration, one can imagine that that may be the field C interacts with. And C has a history of using int as its integer type. If that's the case in your project, you should be using c_int instead of u32 (but of course if they are using uint32_t like my example does, then it is fine).

3 Likes

I did comment on this in my second paragraph; I just also felt the need to comment on the “but this seems unnecessary as this works perfectly fine like this already” thing.

1 Like

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