How to use snake case names in FFI with less casting

I'm working on an FFI project, and exporting a variety of structs to C. I want C to treat these as opaque structs, so C can hold a pointer to them, but cannot access fields (because generally these are not #[repr(C)]). I also want these structs to have C-like names, using snake case instead of camel case.

Right now my code looks like below. For each MyThing I also define an opaque struct my_thing for C's point of view. When giving pointers to C, I cast from &MyThing to *const my_thing, and when receiving pointers from C, I cast from *const my_thing to to *const MyThing.

It's that latter conversion that worries me. It would be easy to accidentally cast to the wrong thing and not notice. In fact, I've already made that mistake a few times, between mut and const. Ideally I'd like some way to say "my_thing and MyThing are the same" (like a type alias) and not have to us as so much. Any thoughts or ideas about what I could be doing better here?

An additional goal is that cbindgen should not export MyThing in the generated .h file.

https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=4394cae1ba285e9a9145fb6f6afa53ef

#[derive(Debug)]
pub struct MyThing {
    x: f64,
    name: String,
}

#[allow(non_camel_case_types)]
pub struct my_thing {
    _private: [u8; 0],
}

#[no_mangle]
pub extern "C" fn thing_new() -> *const my_thing {
    let bx = Box::new(MyThing {
        x: 0.0,
        name: "default".to_string(),
    });
    Box::into_raw(bx) as *const _
}

pub extern "C" fn thing_print(thing: *const my_thing) {
    let thing: &MyThing = unsafe {
        match (thing as *const MyThing).as_ref() {
            Some(t) => t,
            None => return,
        }
    };
    println!("{:?}", *thing);
}
1 Like

It feels like macros could be a big help here, even just macro_rules!. Something like

macro_rules! cast_const_ptr {
    ( $c_type:ty -> $rust_type:ty ; $ptr:expr ) => {
        {
            let ptr: *const $c_type = $ptr;
            ptr as *const $rust_type
        }
    };
}

would let you write cast_const_ptr!(my_thing -> MyThing; thing) and ban as in the rest of the code.

With a proc macro you could even automate the CamelCase <=> snake_case conversions but that might be a heavier approach than you prefer to use here.

2 Likes

Actually, what I left out in the interests of brevity is that I am already using macros:

let config: &mut ClientConfig = try_ref_from_ptr!(config, &mut ClientConfig, ());

But I have to specify the casted-to type as an argument to the macro, and if I get it wrong there is no warning. Proc macros are an interesting idea, though so far I have avoided them to minimize complexity.

1 Like

What about a trait approach?

trait CastConstPtr {
    type RustTy;
    
    fn cast_const_ptr(ptr: *const Self) -> *const Self::RustTy {
        ptr as *const _
    }
}

Then you impl CastConstPtr once for each C type and use CastConstPtr::cast_const_ptr(thing) everywhere instead of as.

2 Likes

Ooh, I like this! Thank you, I'll try it.

1 Like

Here's how I wound up implementing it. Works great! Use a trait to constrain pointer casting by jsha · Pull Request #73 · abetterinternet/crustls · GitHub

1 Like

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.