Pass struct to C

When I pass a struct directly to a C function, it sometimes, maybe usually works, but sometimes the call parameters are garbled.

extern "C" {
    pub fn xyz(arg: FourFloats, a2: *const ::std::os::raw::c_char
         a3: u32, a4: u32, a5: u32) -> *mut Y;
}

xyz in this case gets a2 in the first argument position, but there's probably more wrong with the call stack than that. ( It's reliable - a function that has this problem will always have it - but not consistent. I can't easily replicate it.)

However, if I change the first parameter to arg: &Fourfloats, it works as expected, and this also seems to work with those lucky functions that already successfully received structs by value.

So that's all by way of asking: is &val supposed to be OK for passing structs by value to C? Is this stuff documented anywhere?

This is only about the Rust side of the external function. The C function itself continues to expect pass by value, xyz(struct FourFloats, ...)

1 Like

Well what's the definition of FourFloats that you're using in Rust and in C? Do they match? Are you using repr(C)? Did you use bindgen?

1 Like

In fact I did use bindgen. Definitions seem to be OK, as in other cases the same struct can be passed to C successfully. Maybe in every case, if preceded with &.

Perhaps I could ask the question a different way: what's the difference between passing a struct to C with, and without, the & operator?

I’m not sure I understand this explanation. In what sense is it “reliable” and what’s the scope of “always” in “will always have it” – and then what do you do to see the “not consistent” and how is it hindering “replicating” the issue?

A rust parameter &Foo where Foo: Sized is always ABI-compatible with a pointer -- Foo const*, void*, etc -- on the C side.

Passing by value is a horrible mess of special cases depending on your ABI. Sometimes it'll be by pointer anyway, but sometimes not.

What I meant was, I have functions that have this problem, but I don't know what I have to do, to write an example function that has this problem. So far, example functions work - same struct, etc. The function I need to work, always fails (reliably) if I don't use '&', but some functions work and some fail.

That's interesting. It appears that, for a C function when passed a 16 byte struct, whether declared as pass by value or by pointer, it's the same. I.e. actually passed by pointer, but the receiving function can declare the parameter as a value. On this platform, anyway.

Ah okay, so you’re describing it’s hard to replicate for a simplified/minimized example – I suppose if replicating it in an “example function” turns out hard, you might be implying that your full code isn’t publicly shareable, is it?

It’s surprising since if it’s really the API that’s the problem, you should only really need to keep the function signatures and datatype definitions identical, which should already make for a small example, right?

Nonetheless, perhaps you can share the concrete definitions for FourFloats – both in C code and in (possibly generated) Rust code; as well as the concrete function signature, maybe someone else can spot something anyways?

1 Like

What does :: before std::os::raw::c_char mean?

Global reference, in case there's a local symbol called std. Things like that are pretty common in generated code.

I can replicate this after all. Starting with the vague conclusion:

The struct in question is defined in C++. Rust is calling a function that's inside an extern "C" { } block. I managed to define my own simplified but illustrative C++ struct, and call my functions. All C functions expect pass by value.

  • if the struct is passed as the only parameter, it comes through fine - every variation of the struct I tried.
  • if passed as the first of 2 parameters (X2):
  • if the struct has only data members, it comes through fine. That's OK.
  • if the struct has a destructor member, in the X2 case it will be passed by pointer.
#include <stdio.h>

class Rectangle {
public:
        float                           left;
        float                           top;
        float                           right;
        float                           bottom;
        // Rectangle(float l, float t, float r, float b): left(l), top(t), right(r), bottom(b) { printf("construct Rectangle\n"); }
        ~Rectangle() { printf("destruct Rectangle\n"); }
};

extern "C" {
void
LW_ew1(Rectangle p1) {
        fprintf(stderr, "foreign ew x 1 ...\n");
        fprintf(stderr, "foreign ew { %5.2f, %5.2f, %5.2f, %5.2f }...\n", p1.left, p1.top, p1.right, p1.bottom);
        fprintf(stderr, "foreign ew ok\n");
}
void
LW_ew2(Rectangle p1, unsigned p2) {
        fprintf(stderr, "foreign ew x 2 ...\n");
        fprintf(stderr, "foreign ew { %5.2f, %5.2f, %5.2f, %5.2f }...\n", p1.left, p1.top, p1.right, p1.bottom);
        fprintf(stderr, "foreign ew ok\n");
}
}

#![allow(non_snake_case)]
#![allow(dead_code)]
#![allow(non_camel_case_types)]

#[repr(C)]
#[derive(Debug, Copy, Clone)]
pub struct Rectangle {
    pub left: f32,
    pub top: f32,
    pub right: f32,
    pub bottom: f32,
}

#[repr(C)]
#[repr(align(8))]
pub struct WL {
    pub _bindgen_opaque_blob: [u64; 1],
}

#[link(name = "cwrap")]
extern "C" {
    pub fn LW_ew1(p1: Rectangle);
    #[link_name = "LW_ew2"]
    pub fn LW_ew1b(p1: &Rectangle, p2: u32);
    pub fn LW_ew2(p1: Rectangle, p2: u32);

}

impl WL {
    pub unsafe fn ew(p1: Rectangle) { 
        println!("WL::ew({:?}, ...)", p1);
        LW_ew1(p1);
        println!("(passing &Rectangle ...)");
        LW_ew1b(&p1, 9);
        LW_ew2(p1, 9);
    }
}

extern crate tlx;
use tlx::wl::{WL,Rectangle};

fn main() {
    let r = Rectangle { left: 209.0, top: 308.0, right: 407.0, bottom: 506.0 };
    unsafe { WL::ew(r) };
}

Aha! It’s not a C struct, but a C++ class!

Without further ado, here are C++ features that bindgen does not support or cannot translate into Rust:

  • […]
  • Many C++ specific aspects of calling conventions. For example in the Itanium abi types that are "non trivial for the purposes of calls" should be passed by pointer, even if they are otherwise eligible to be passed in a register. Similarly in both the Itanium and MSVC ABIs such types are returned by "hidden parameter", much like large structs in C that would not fit into a register. This also applies to types with any base classes in the MSVC ABI (see x64 calling convention). Because bindgen does not know about these rules generated interfaces using such types are currently invalid.

following the first link https://itanium-cxx-abi.github.io/cxx-abi/abi.html#non-trivial:

non-trivial for the purposes of calls

A type is considered non-trivial for the purposes of calls if:

  • it has a non-trivial copy constructor, move constructor, or destructor, or
  • all of its copy and move constructors are deleted.

This definition, as applied to class types, is intended to be the complement of the definition in [class.temporary]p3 of types for which an extra temporary is allowed when passing or returning a type. A type which is trivial for the purposes of the ABI will be passed and returned according to the rules of the base C ABI, e.g. in registers; often this has the effect of performing a trivial copy of the type.

So yes, structs/classes with a destructor passed by value are officially unsupported.

3 Likes