Interfacing C code with bindgen: #define and types

Hello everyone,

I started using bindgen and the cc along with a Cargo build script to interface a C library.

I'm facing two issues. First of all, function-like macros don't seem to be considered. There is a ticket #753 on that in regard to macros with parameters that invoke other macros. But also "functions" implemented as macros (which are commonly found in some C header files) seem to be ignored, like:

#define example_print(x)   example_print_impl((x), 0)
#define example_println(x) example_print_impl((x), 1)

I'll have to write a wrapper for all these sort of "functions" (which are actually macros). I wonder if there's any good way or practice to work around that? But I assume there isn't, right? :frowning:

The other issue is that apparently integer constants (coming from header files using #define) are assumed to be of type u32 by default. When I want to use these constants in a place where a std::os::raw::c_int is expected, I get an error on mismatched types (expected i32, found u32). I can fix that by adding

builder = builder.default_macro_constant_type(
    bindgen::MacroTypeVariation::Signed
)

(at least as long as I don't use other types).

This causes i32 to be used instead of u32, but I was a bit surprised that the code then compiled, and I didn't even get any warning because an int in C is not always equal to i32. Rust's documentation says:

Type Definition std::os::raw::c_int

pub type c_int = i32;`

Equivalent to C’s signed int (int) type.

This type will almost always be i32, but may differ on some esoteric systems. The C standard technically only requires that this type be a signed integer that is at least the size of a short; some systems define it as an i16, for example.

So I see now why I don't get a warning, but I feel a bit insecure here. Wouldn't it be better if c_int was a distinct type? Can I just ignore this as long as I don't plan to port my code to small microcontrollers where c_int might be i16 only? Do other people distinguish between c_int and i32 in their code?

1 Like

You're seeing the documentation as generated for a specific platform. E.g. c_char shows an alias to i18, but on some platforms it's an alias to u8. It looks c_int is currently i32 on all platforms, but that's not something you can portably assume.

(If it wasn't an alias, you'd probably have 0i32 as c_int everywhere, and then have bugs when a non-i32 came around instead of compile errors. At least I'm guessing that's the logic.)

That means, assuming my constants are fed into functions which expect a c_int, then I have to write something like my_c_api::SOMECONST as c_int to keep my code portable? And I would get no warning whenever I forget the as c_int? I.e. missing it in a single place could later cause compile errors on other platforms (which I won't see or notice on my platform)?

That's a bit unfortunate.

If c_int really was a different type, couldn't you have macros for constants?

If you want your types to vary per platform like they do in C, use c_char and c_int and friends; write your functions to expect c_int; write your constants as SOMECONST: c_int; don't assume c_int is an i32. If you want to be portable, don't use as period.

If you happen to intermix i32 and c_int somewhere without conversion and then compile for a platform where they don't match, you'll get an error, and can then change it to c_int or #[cfg()] a bunch, etc.

I guess you're wishing it was another platform-varying primitive type, ala usize/isize? Or maybe newtypes like this comment. With newtypes you lose some utility with literals and inference and whatnot.

1 Like

I will likely not always work with c_int. Some API's also utilize stdint.h with int32_t and so on, where things should be easier.

Regarding constants originating from #define I will just be careful with always casting (small) constants originating from #defines to whichever type needed (these don't really have a type in their definition as they are just text replaced by the C preprocessor).

I think when they are small (enum like), then writing CONST as _ isn't a too bad thing to do?

I dislike how bindgen just assumes some particular integer type for them (not even using c_int, apparently), and I wonder if it would be better if these #defined constants were emitted as Rust macros instead of constants?

Actually I had newtypes in mind, but didn't think this through. It might be a syntactical nightmare.

It's not too bad I guess, but is it necessary? Probably I'm just lacking context (but asked if it's necessary because things like this are an error:)

type c_int = i8; // yikes
const THING: c_int = 300;

Do you mean a macro with a tree of #[cfg()]? The idea is you re-bindgen per target and this is also the philosophy of libc. You're generating the pre-processed Rust equivalents, not a cross-platform glue library.

Yes, I believe, to create an equivalent of #define SOMECONST 1 in Rust.

I will provide an example below.

No, I actually mean a simple "Macro By Example" to replace SOMECONST!() with, for example, 1, like it's done in C too.

Currently #define MYCONST 1 gets translated to:

pub const MYCONST: u32 = 1;

or

pub const MYCONST: i32 = 1;

But wouldn't it be better if it was emitted by bindgen as:

macro_rules! MYCONST { () => { 1 }; }

I'm asking because in C, I can pass MYCONST to both functions expecting an u8 or an int. It's not specified/defined by the #define which type the 1 actually has. So I was thinking whether it would be good to reflect that in the generated binding.

Consider the following code, which does compile unless I uncomment the commented out code:

macro_rules! MODE_FORWARD { () => { 1 }; }
macro_rules! MODE_REVERSE { () => { 2 }; }

const MODE_FORWARD: std::os::raw::c_int = 1;
const MODE_REVERSE: std::os::raw::c_int = 2;

fn takes_int(mode: std::os::raw::c_int) { /* … */ }
fn takes_u8(mode: u8) { /* … */ }

fn main() {
    takes_u8(MODE_FORWARD!());
    takes_int(MODE_REVERSE!());
    // One of these will fail to compile:
    //takes_u8(MODE_FORWARD);
    //takes_int(MODE_REVERSE);
    // But we can do:
    takes_u8(MODE_FORWARD as _);
    takes_int(MODE_REVERSE as _);
}

(Full example in Playground)

Note that in C, we would have:

#define MODE_FORWARD 1
#define MODE_REVERSE 2

where 1 and 2 have no type. They are just inserted into the source code as text by the preprocessor.


A different case would be if we had:

#define SOME_INT_CONST ((int)3)
#define SOME_i32_CONST ((int32_t)3)
#define SOME_U8_CONST ((int8_t)3)

These could be (safely) converted to:

pub const SOME_INT_CONST: std::os::raw::c_int = 3;
pub const SOME_I32_CONST: i32 = 3;
pub const SOME_U8_CONST: u8 = 3;

But bindgen doesn't work with these at all and will just emit nothing. Anyway, it's not what we usually find in C headers. We will find instead:

#define SOME_CONST 3

And picking one of c_int, i32, or u8 for that just seems wrong.

I get it now. I have no idea how receptive people would be, but maybe you can float the macro-for-constants concept in a bindgen issue.

Or you could change the glue.

const MODE_FORWARD: u8 = 1;
const MODE_REVERSE: u8 = 2;

fn takes_int<T: Into<std::os::raw::c_int>>(mode: T) { /* ... */ }

Or (to be macro'd)

mod _mode_forward {
    #![allow(unused)]
    use std::os::raw::c_int;
    const C_INT_SIZE: usize = std::mem::size_of::<c_int>();
    const BITS_FOR_U8: usize = 8 * (C_INT_SIZE - 1);
    const MODE_FORWARD_BITS: usize = c_int::leading_zeros(super::MODE_FORWARD) as usize;
    const GUARD: u8 = 1 / (MODE_FORWARD_BITS >= BITS_FOR_U8) as u8;
    pub const MODE_FORWARD_U8: u8 = GUARD * super::MODE_FORWARD as u8;
}
use _mode_forward::MODE_FORWARD_U8;

(There's probably a compile-time cast-checker in the ecosystem somewhere :sweat_smile:)

Where do you get such ideas!? :astonished:

Note that in my case, functions like takes_int or takes_u8 will be C functions, and those (usually) have a concrete type for each of their arguments (and I expect an error from the Rust compiler if I try to pass anything else). It's the #define macro in the C world which (usually) doesn't indicate a type.

Changing the function here doesn't seem like the right thing to do. The issue is with the constants.

I feel like the only clean approach is to use macros in Rust for #defined constants (unless someone can come up with a better idea?). I can see how it might be perceived as a bit ugly or as a "hack" from a high-level-programming point of view. However, a macro is exactly what we have in C. Acting like we have a typed constant is just not how the C API is designed in case of #defined constants in the header files.

I'll seriously think about opening an issue on this (but of course also happy for any feedback here on my thoughts/idea). I wonder if I'm the only person with this problem, because interfacing C API's with constants in the header files seems to be a common thing to do. I'd claim 99% of all C APIs have some sort of constant defined by using macros in the header files. Maybe most people just use MacroTypeVariation::Signed and have no practical problems (until they port to a different system)… Or they explicitly cast every constant they use, which you have to do if you use the default configuration and want to pass a constant to a function expecting a C integer… unless the value is negative, as then i32 will be used disregarding the setting… which then ends in non-portable code again if you forget the cast :frowning_face:.

I just opened an Issue #2120, "Avoid assigning (wrong) types to constants using #define".

When you've seen enough dirty tricks I guess...

Someday we'll have const_panic.

The way unsafe works in Rust is, you package it up in tiny quarantined areas, and then hopefully are able to build a safe API around those. Then you program in normal Rust against the safe API.

All FFI is unsafe.

Instead of programming Rust like it was C, you should wrap your bindgen-erated glue up in Rust functions that take care of all the invariants. Then you can program Rustically against the C code. When I referred to receptiveness (or lack thereof), this is what I was thinking of -- making the macros will encourage programming "paste this text" C-style on the Rust side, and even for those who write the idiomatic, Rustic wrappers will have the option to blindly apply the macro inside the wrapper functions instead of stopping to think about the invariants (and then changing the generated type to u8 or writing a compile-time guard, etc.)

Not sure if I understand you right.

I feel like creating as-small-as-possible wrappers would create a lot of noise in my code (which still will be unsafe, see my use-case below). It would almost require me to re-write each function of the C API that I want to use. And even if I do, I might still pass a constant to some function within one of these many wrappers. And given the current behavior of bindgen, that would lead – in the worst case – to no error but to code that won't compile on certain other platforms (without me noticing).

I wouldn't be surprised if a lot of Rust code that uses FFI isn't portable (even where it could easily be made portable).

I do like the Rust types, and it's so much nicer to use u8, i32, usize, etc. than dealing with int, long, etc. in C. But I think we need to be very very careful when creating bindings and reflect the C types (or C "not-a-type"s) as correct as possible. (Thinking also on the thread "usize is not size_t" here, which I haven't fully read.)

My use case requires using the Lua C API. Working with that API usually involves a lot of several subsequent function calls that are dependent on another. A lot of things can go wrong here.

Of course, I could also try to do the work to create wrappers (or use a crate) which provide(s) safer Rust interfaces for the whole Lua C API (and then add another abstraction layer, which is also part of what I want to do). But there'll be some friction and possibly also loss of performance.

I understand that at some point it's necessary to create safe interfaces. But I see bindgen as tool to make C calls in Rust, and that's what I need to do. And here it behaves oddly (as I tried to explain with my posts). I won't need bindgen for higher-level interfaces, of course.

Anyway, perhaps I didn't get your point. I'm still very new to connecting Rust with C, and I'm still trying to figure out what works best. I currently ended up writing some extensions in C, doing some C-style-programming in Rust, and created some clean Rust interfaces that do not expose any pointers, std::os::raw types, etc. at all. I don't know if my choice in these cases is well-balanced yet, but I think I'll learn from experience and keep your advice in mind to not simply "paste" my C code in Rust :grin:. But where I need to call C, I would like bindgen to support me as good as possible.

If you care that much about portability being checked by the compiler (which is a very legitimate stance, don't get me wrong! I don't know where but I remember that the meta-level with #[cfg]s was one of Rust weak points, since it's hard to ensure lack of compiler errors or code coverage when code has been #[cfg]-elided by some sort of pre-processor), I do highly advise that you define transparent newtype wrappers:

mod strict {
    #[repr(transparent)]
    pub
    struct c_int(::std::os::raw::c_int);

    match_! {( i8, i16, i32, isize, i64, u8, u16, u32, usize, u64 ) {
        ( $($T:tt),* ) => ($(
            impl TryFrom<$T> for c_int {
                type Error = <$T as TryFrom<::std::os::raw::c_int>>::Error;

                fn try_from (x: $T)
                  -> Result<Self, Self::Error>
                {
                    x.try_into().map(Self)
                }
            }

            /* maybe add saturating and wrapping conversions as well */
        )*);
    }}
}
  • with match_! being a tool (ideally named match!) for ad-hoc macro invocations.

And then tell bindgen to use crate::strict:: as the prefix for the integer types.

Thanks to #[repr(transparent)], the ABI will be the same as that of the external C libs, and you'll have guaranteed type safety similar to that of usize never being the same as u32 nor u64.

The only annoyance will be that you'll have to use .try_into()s in many places (you won't have the "ergonomics" of as, although when you think about is, the footguns of as make it a rather bitter-sweet sugar…).


Also, I agree with the macro approach for the constants, that would be the most sensible translation of #defines, but the !() noise and, more generally, macros as a whole, don't get that much love in the ecosystem and are avoided when possible.

I don't fully understand your code above.

This is a newtype wrapper for c_int.
Is it intended to shadow std::os::raw::c_int?

What is match_!? You write:

What kind of tool? I don't understand.

Yes, I agree using as should be avoided due to problems/bugs at runtime on overflow. When I have a value where I don't know whether it will fit, I should use .try_into(). But in case of constants, that should rather be decided at compile time anyway, right?

Sorry, I wrote the post at a "conceptual" level trying not to delve too much into the details. The idea was to see a bunch of TryFrom implementations thanks to "macros" (in a wave-handed fashion), while also showcasing a potential syntactic improvement that has been on my mind for a while.

match_! can be written and implemented as:

macro_rules! match_ {(
    ( $($input:tt)* ) $rules:tt
) => (
    macro_rules! recurse $rules
    recurse! { $($input)* }
)}

and is basically a potential shorthand for doing things like:

macro_rules! try_from_impls {( $($T:tt),* $(,)? ) => ($(
    impl TryFrom...
)*)}
try_from_impls! { i8, i16, ..., u64 }
rationale

I've never been super fond of ordering the items like that (macro def before macro call), since macros are generally hard to read, and when defined in this order, the inputs to the macro are only known later on; which is information that would help the readability of the macro definition.

I thus prefer doing:

try_from_impls! { i8, i16, ..., u64 }
// where
macro_rules! try_from_impls {( $($T:tt),* $(,)?? => (
                            // ^^^^^^^^^^ 
                            // we know here that `$T` stands for `i8`, `i16`, etc.
    ...
)}
use try_from_impls;

but then we have a try_from_impls identifier being repeated three times for just one invocation of the macro. Hence the match_! shorthand.

aside

And if that were blessed to be a language construct (match! syntax), then it could also let itself to chaning macros / eager expansion, in a rather controlled fashion:

match! concat_idents!(foo, bar) {( $foobar:ident ) => (
    fn $foobar() { ... }
)}
// or
match! include_str!("...") {( $contents:tt ) => (
    my_proc_macro! { $contents } /* problem of proc-macro accessing the fs solved. */
)}

Yes.

It is intended to shadow that type w.r.t. bindgen's generated definitions:

  • Here is the relevant part of ::bindgen's API that allows to do that.

Basically you have, in C, an int type, and the question that you and/or bindgen asks is which type, in Rust, should represent it. And it turns out we are free to choose whichever we want, provided it have the same ABI as a C integer: same layout, and same calling convention.

By construction, ::std::os::raw::c_int or ::libc::c_int are both types which satisfy these conditions, but they have the huge caveat of being defined as type aliases w.r.t. one of the Rust primitive integer types (e.g., i32), which makes the conversion between such a type and these aliases too lenient / unchecked on most platforms, and fail to compile on the platforms where the alias points to something else.

  • That is, these are the same problems that we'd have if usize had been defined as:

    #[cfg(target_pointer_width = "32")]
    type usize = u32;
    
    #[cfg(target_pointer_width = "64")]
    type usize = u64;
    

    Luckily usize is a type that is always distinct from both u32 and u64, which makes code have to explicitly perform some kind of transformation between these (in this instance, often as overused as casts, or as preferred .try_into() checked casts, or, for more fine-grained control over the cast's behavior, third-party crates out there feature quite nifty APIs, such as ::az.

So the question then becomes: could we have a type

  • with the same ABI as C's int type (i.e., same ABI as ::std::os::raw::c_int),

  • but distinct from the primitive integer types?

Achieving the latter is trivial: just define a new type (e.g., a struct). And for the former, we just mark that newtype as being #[repr(transparent)] over a type with the desired ABI. Hence:

#[repr(transparent)]
pub struct strict_c_int(::std::os::raw::c_int);
  • Aside: feel free to slap #[derive(Debug, Clone, Copy, PartialOrd, Ord, PartialEq, Eq, Hash)] onto it

At that point, thanks to having an actually distinct type, we can use privacy to encapsulate its construction i.e., to only offer explicitly checked casts!

And finally, to reduce the boilerplate with cbindgen, as well as avoid the "redundancy" of strict::strict_c_int;, I chose to rename it the same as c_int.

  • But if you find yourself in a codebase already using the "fuzzy" c_int from stdlib, and this new one, then shadowing may not be that well suited (although you could always "unshadow" as needed: use strict::c_int as strict_c_int;).

  • I personally, on the contrary, love shadowing the ill-designed types with these more robust ones as often as possible, to reduce the chances of accidentally using a "lenient" c_int, and so on.

Yes, the missing part is the case of constants (or at least literals). This a typical problem in the Rust language, since we'd like the same for, for instance, &'static CStr (we'd like b"Hello, World!\0" to be somehow convertible to a &'static CStr without runtime errors, and b"Ill F\0rmed" to trigger a compilation error). Similar to NonZeroU64 and other similar types featuring a const fn construction, etc. etc.

There is currently no auto-generated tool for these things, although const fn can get you quite far.

Let's start with NonZeroI32::new(), for instance:

macro_rules! nz_i32 {( $N:expr $(,)? ) => (
    {
        const __N: ::core::primitive::i32 = $N;
        {
            use ::core::{prelude::v1::*, num::NonZeroI32};
            const NZ: NonZeroI32 = match NonZeroI32::new(__N) {
                | Some(nz) => nz,
                | None => {
                    #[forbid(const_err)]
                    let _panic = 0 / __N;
                    [][0]
                },
            };
            NZ
        }
    }
)} pub(in crate) use nz_i32;
  • Playground

  • The key idea is that through helper const blocks, we can ensure come const fn fallible computation (such as NonZero::new()) is computed at compile-time, so as to make the failures become always-caught compile-time errors rather than runtime failures that may dodge unit tests.

    Since a const block is cumbersome to write (until we get inline const { ... } blocks), and "compile-time panics" require getting creative (although we'll soon have proper compile-time panics!), the current solution to alleviate this pattern / feature a lightweight construction is to use macros.

Another options is to use a procedural macro (or a build.rs script, but that's awfully cumbersome to write!) to inspect the contents of a given literal (≠ a const), and only emitted an unchecked construction if no invalid input is detected.

  • But that is brittle (what if the given const is not a literal, but the name / path to another constant (e.g., N); what if the given const is the output of some other macro?), so it should be avoided when a const fn-based implementation can be featured.

A final solution, which does have the advantage of not requiring to think too hard about const fns nor proc-macros, is, for the case of literals, again, to generate an anonymous test (thus a doctest) that will eagerly check that the input does not cause failures. It does have the issue that the input will still be checked at runtime each time, but does have the advantage of guaranteed a lack of runtime panics should the doctests have been run before releasing the code.

macro_rules! doctested_nz_i32 {( $N:literal $(,)? ) => ({
    /// ```rust
    #[doc = stringify! {
        let _ =
            ::core::num::NonZeroI32::new($N)
                .unwrap_or_else(|| panic!(
                    "`doctested_nz_i32!({:?})` failed", $N,
                ))
        ;
    }]
    /// ```
    extern {}

    ::core::num::NonZeroI32::new($N).unwrap()
})} use doctested_nz_i32;

In the case of CStrings, there are several crates out there which feature this compiletime-checked & runtime-infallible CStr constants. I'll mention the case of

since it features a "modern" const-friendly implementation (feature-gated for MSRV reasons), as well as a historical proc-macro implementation

From all these examples it shouldn't be that difficult to feature similar macros for runtime-unfallible construction of strict::c_ints out of numeric constants (of any type, for what is worth):

#[macro_export]
macro_rules! const_c_int {( $N:expr $(,)? ) => ({
    #[forbid(const_err)]
    const __C_INT: $crate::strict::c_int = {
        const MIN: ::core::primitive::i128 = ::std::os::raw::c_int::MIN as _;
        const MAX: ::core::primitive::i128 = ::std::os::raw::c_int::MAX as _;
        match $N as i128 {
            ok @ MIN ..= MAX => unsafe { ::core::mem::transmute(ok as ::std::os::raw::c_int) },
            _ => [][0],
        }
    };
    __C_INT
})}

but then such a construction would feature the same portability issues as we initially had: a const_c_int!(42) passing is not that different from a let it: c_int = 42; : for a big enough constant, insome platforms both forms may pass compilation but on others it may fail.

So the right thing to do here would be to either remain open to the possibility that even literal numbers can have a fallible conversion (since, even if the input number is constant, the target size is not, portability-wise!), or to choose to support integers only up to a specific integer size which you know shall fit the choice of ::std::os::raw::c_int for all platforms (is that i16?).

The macro above can then be amended to use such lower bounds for MIN and MAX (rather than those of c_int). Assuming i16 is OK:

- const MIN: ::core::primitive::i128 = ::std::os::raw::c_int::MIN as _;
+ const MIN: ::core::primitive::i128 = ::core::primitive::i16::MIN as _;
  // etc.

Understood now. It was just a bit tough for me to digest all of that at one time :sweat_smile:

Basically you create a newtype for c_int (same name as but distinct from std::os::raw::c_int) with a bunch of TryFrom implementations for all primitive integer types.

To keep it concise, this is done via a macro (match_!) that invokes, for each $input, an inner macro (recurse!) with the same $rules which process each $input.

:woozy_face:

(feeling double wrapped and innerly recursed now)

Thanks for sharing this interesting macro concept though. I might use it.

Ah okay, so it isn't really shadowing, but explicitly telling bindgen which prefix to use.

Yes, I totally agree, and I think it's dangerous to define c_int as i16 or i32 (in contrast to usize, where it's deliberately not done), because it will make code that compiles without problem on one platform fail to compile on different platforms.

So actually the problem isn't just limited to #defined constants but a more general issue. However, IMHO it would still be wrong to assume a type strict::c_int for a #defined constant.

Perhaps this could be solved if we created a distinct type like c_literal that then somehow uses const fns for conversion to other types? Maybe that could be an alternative to my proposal to use macros for constants in C header files that are "defined" through #define. I understand too little about the implications yet to judge whether that's worth the effort (Edit: or even feasible).

Not sure if I understand the rest of your post already, but:

But the target size is known at compile-time. It is okay to encounter an error (at compile time!) when porting a program that relies on a certain value fitting in a (platform dependent) type. But what I would not like is:

  • reporting the error at runtime (when it could have been reported at compile time), or
  • causing errors (whether at compile time or runtime) just because a (non-failing) conversion wasn't made (because the programmer wasn't warned about it being necessary).

If the error happens because someone tries to fit a large constant into a c_int, then I see no problem if this causes trouble when trying to port the program. But in my originally viewed case, this also happens with small numbers like 1 or 2 or 3.

(But like I said, I might not have fully understood the second part of your post yet. Maybe you did mean failures at compile-time?)

AFAIK an int in C must be at least 16 bits wide, so yes to your question.

1 Like

Rethinking about that, there might be some scenarios where it might be better if there was a runtime error thrown when a literal is used in a place where it doesn't fit a particular C type (as the literal might only be executed in certain cases). But I don't think it makes sense in the general case.

I don't have opinions on which strategy is better, but I have written my post thinking that a checked runtime error (not an implicit-overflow-induced logic error: we both agree that that's the worst! :grinning_face_with_smiling_eyes:) was better than a compile error where the whole project would not compile. Hence what I mentioned.

But I can definitely see the rationale behind prefering a compile-error to a runtime error, in which case the const_c_int! that checked against ::std::os::raw::c_int at compile-time would be a more sensible default :+1:

The more difficult case is when a value is returned and I want to compare it to a #defined constant. This would be a very bad idea:

// bad, because casting can truncate
if ffi_mod::some_c_func() as u32 == ffi_mod::SOME_CONST {
}

So I have to write it on the right side:

if ffi_mod::some_c_func() == ffi_mod::SOME_CONST as _ {
}

The second code should be safe if some_c_func's return type is big enough to contain the expected constant return value (which seems to be a reasonable assumption for most APIs).

But when I go for the second approach, I cannot use match, but I have to use a real ugly syntax (x if x == SOMECONST as _):

fn main() {
    const MYCONST: u32 = 2;
    let i: std::os::raw::c_int = 2;
    match i {
        //x as _ => println!("MYCONST found"),
        x if x == MYCONST as _ => println!("MYCONST found"),
        _ => println!("other value")
    }
}

(Playground)

Now you could say: Don't use casts anyway. They are dangerous. Okay, let me try a clean approach with try_into:

fn main() {
    const MYCONST: u32 = 2;
    let i: std::os::raw::c_int = -1;
    match i.try_into().unwrap() {
        MYCONST => println!("MYCONST found"),
        _ => println!("other value")
    }
}

(Playground)

But the code above will create a runtime error!

How if I go for:

fn main() {
    const MYCONST: u32 = 2;
    let i: std::os::raw::c_int = -1;
    match i {
        x if x == MYCONST.try_into().unwrap() => println!("MYCONST found"),
        _ => println!("other value")
    }
}

(Playground)

This seems to work. I assume the last approach is what I should do? It should never panic as long as we can assume that the constant fits into the type of the tested variable (which is a reasonable assumption, as mentioned above). But under that assumption, I could also just write x if x == MYCONST as _, which is at least a bit shorter.

So I'm not really sure what to do. match will force me to use an ugly x if x == … syntax, whatever I do. Or is there any other way?

If MYCONST was a macro, things would be much more easy:

macro_rules! MYCONST { () => { 2 }; }

fn main() {
    let i: std::os::raw::c_int = 2;
    match i {
        MYCONST!() => println!("MYCONST found"),
        _ => println!("other value")
    }
}

(Playground)

I hope this explains better why I want #defined constants being emitted as macros by bindgen.

How do other people deal with these issues?

  • Do you ever use match statements in your FFI code?
  • How do you deal with type mismatches, given the current behavior of bindgen?
1 Like

Regarding that, I think the correct code would be:

    const MY_CONST: u32 = 2;
    let i: std::os::raw::c_int = -1;
-   match i.try_into().unwrap() {
+   match i.try_into() {
-       MY_CONST => println!("MY_CONST found"),
+       Ok(MY_CONST) => println!("MY_CONST found"),
        _ => println!("other value")
    }

Since the idea is that if, mathematically, i has the same numeric value as MY_CONST, then that i instance must fit / be losslessly convertible to the type of MY_CONST. The contraposition thus justifies that second match arm :slightly_smiling_face:.

  • (I don't disagree with the rest of your point, though, about the usefulness of transposing #defines to Rust macros)
1 Like