Skip to content

Cannot pass small struct by value across FFI on linux-gnu #59187

Open
@amberjhu

Description

@amberjhu

I am creating an FFI function that takes a tagged union as a parameter. It corresponds to a Rust enum with variant. The tagged union definition for the C++ code was generated with cbindgen. The problem is that the calling convention between Rust's extern "C" and the actual C calling convention aren't matching up, so it gets faulty values for the argument.

This looks like it's similar to #5744.

I tried this code:

Rust:

#[repr(C)]
#[no_mangle]
pub enum SLCArgs {
  Add(*const c_char), // borrowed
  Count,
  Print,
}

#[no_mangle]
// works if changed to extern "Rust"
pub unsafe extern "C" fn StringListContainer_do(slc_p : *mut StringListContainer, m : SLCArgs) {
  let slc = &mut *(slc_p);
  match m {
    SLCArgs::Count => println!("{}", slc.Count()),
    SLCArgs::Add(cstr) => StringListContainer_Add(slc_p, cstr),
    SLCArgs::Print => slc.print(),
  }
} 

bindings.h:

struct StringListContainer;

struct SLCArgs {
  enum class Tag {
    Add,
    Count,
    Print,
  };

  struct Add_Body {
    const char *_0;
  };

  Tag tag;
  union {
    Add_Body add;
  };
};

extern "C" {
void StringListContainer_do(StringListContainer *slc_p, SLCArgs m);
//...
} // extern "C"

C++:

int main() {
  StringListContainer* slc = new_StringListContainer();

  StringListContainer_do(slc, {SLCArgs::Tag::Add, {"top text"}});
  StringListContainer_do(slc, {SLCArgs::Tag::Add, {"middle text"}});
  StringListContainer_do(slc, {SLCArgs::Tag::Add, {"bottom text"}});
  StringListContainer_do(slc, {SLCArgs::Tag::Count, {}});
  StringListContainer_do(slc, {SLCArgs::Tag::Print, {}});
}

I expected to see this happen:

3
top text
middle text
bottom text

Instead, this happened:

It prints nothing out because Rust believes that the struct was placed in the argument build area of the stack, just above the return address. In reality, since this is a small struct, it was placed in the second and third registers %rsi %rdx.

According to this interpretation of the System V AMD64 ABI, structs between 2-4 words are passed sequentially through registers. Although, according to some experimentation, it looks like any struct over 2 words is also passed on the stack.

Looking at the assembly, the first SLCArgs was initialized on the stack in the argument build area just by chance, so the values that Rust reads when it looks for the struct is Add("top text"). However, this happens every time we call StringListContainer_do, so we never print anything out, just keep adding the first string.

However, this does work on Windows-MSVC and on Linux-GNU but only if you mark StringListContainer_do as extern "Rust". Just extern and extern "C" will not work.

The repo for this code is here, if that helps

Meta

rustc --version --verbose:

rustc 1.33.0 (2aa4c46cf 2019-02-28)
binary: rustc
commit-hash: 2aa4c46cfdd726e97360c2734835aa3515e8c858
commit-date: 2019-02-28
host: x86_64-unknown-linux-gnu
release: 1.33.0
LLVM version: 8.0

Metadata

Metadata

Assignees

No one assigned

    Labels

    A-FFIArea: Foreign function interface (FFI)C-bugCategory: This is a bug.O-linuxOperating system: LinuxO-x86_64Target: x86-64 processors (like x86_64-*) (also known as amd64 and x64)T-compilerRelevant to the compiler team, which will review and decide on the PR/issue.

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions