Need help with pointer arithmetic fun for FFI binding over multiple structs

I've written a Rust lib to serve as an FFI binding between a C program and the Rust code behind it (exported as a dylib). My initial attempts were successful, but as I get a bit deeper into my use case I've found myself stuck trying to map Rust's concept of an enum with C's combination of struct + union.

Here's some C code:

typedef struct Foo {
  const char* name;
} Foo;

typedef struct Bar {
  const char* name;
  const char* field;
} Bar;

typedef struct Types {
  const char* typename;
  union
  {
    Foo foo;
    Bar bar;
  };
} Types;

uint8_t rust_parser(char* string, Types** types, size_t* types_len);

int main() {
  Types* types = NULL;
  size_t types_len = 0;
  uint8_t err;

  err = rust_parser("foo\nbar", &types, &types_len);

  if (err > 0) {
    printf("Error: Return code %d", err);
    exit(err);
  }

  for (size_t i = 0; i < types_len; i++) {
    if (types[i].typename == "foo") {
       printf("a foo!");
       printf("and the name is %s", types[i].foo.name);
    }
    else if (types[i].typename == "bar") {
      printf("a bar!");
      printf("and the name is %s", types[i].bar.name);
      printf("and the field is %s", types[i].bar.field);
    }
  }

  return err;
}

Corresponding Rust would be:

#[derive(Debug)]
#[repr(C)]
pub struct Foo {
    name: *const c_char,
}

#[derive(Debug)]
#[repr(C)]
pub struct Bar {
    name: *const c_char,
    field: *const c_char,
}

pub enum Types {
    FooType(Foo),
    BarType(Bar),
}

impl Types {
    pub fn typename(&self) -> *const c_char {
        match *self {
            Types::FooType      { .. } => CString::new("foo").unwrap().into_raw(),
            Types::BarType      { .. } => CString::new("bar").unwrap().into_raw()
        }
    }
}

pub extern fn rust_parser(schema: *const c_char, types: *mut *mut Types, types_len: *mut size_t) -> u8 {
    // Convert C string to Rust string
    let c_schema = unsafe {
        assert!(!schema.is_null());
        CStr::from_ptr(schema)
    };
    let r_schema = c_schema.to_str().unwrap();
    match gqlidl::parse_schema(r_schema) {
        Ok(vec) => {
            let mut tmp_vec: Vec<Types> = vec.into_iter().map(|mut v| {
                // v.shrink_to_fit();

                let s = match v.typename() {
                    "foo" => {
                        return Type::FooType(Foo::new(
                            v.name())
                        );
                    },
                    "bar" => {
                        return Type::BarType(Bar::new(
                            v.name(),
                            v.field())
                        );
                    },
                    _ => panic!("Unknown typename: {}", v.typename())
                };

                mem::forget(v);
                s
            }).collect();
            tmp_vec.shrink_to_fit();
            assert!(tmp_vec.len() == tmp_vec.capacity());

            // Return number of types
            unsafe { *types_len = tmp_vec.len() as size_t; }

            // Return pointer to data
            unsafe { *types = tmp_vec.as_mut_ptr(); }

            // Prevent memory from being deallocated
            mem::forget(tmp_vec);

            0
        },
       Err(err) => {
            writeln!(io::stderr(), "Catastrophic error: {:?}", err).unwrap();
            1
        }
    }
}

I've cut out some of the Rust and C code out for brevity. gqlidl::parse_schema takes a string and spits out a vector of structs. The C portion will hand the string to Rust for parsing, and then it should get back an array of various C structs and act on them. Now, I know the binding originally worked when I did not use typedef struct Types with a union inside. That is, I just assumed everything was of type Foo, and the C portion could read the struct fields. But, when I try to use the program now with multiple types, I get various errors on the C side (like strings equal to (null). That suggests that I'm reading from the wrong memory location.

Is there something obvious (or complicatedly wrong) that I am missing from the above?

You can't use Rust enums from C this way (or any way). To get a type that's compatible with a C union, you have to use a union. This was introduced in Rust 1.19, see Announcing Rust 1.19 | Rust Blog.

2 Likes

Oh, I guess I hit this just in time then. :grin: Have you seen an examples of union used for FFI bindings in the wild?