Description
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