How to use Option<T> to interface with C code taking a possibly-null-pointer to T

Below is a simplified example of the issue I'm facing in larger project. Basically, I have a C library function I need to call that takes a pointer as a parameter. It is an optional parameter that uses NULL to indicate nothingness. Since I'm wrapping the interface in Rust, I of course want to expose that parameter as an Option. However, when I try to take a possibly-null-pointer to the payload of the option and pass that into the function, I get garbage results on the C side. It is pointing to something, but that something is not the right thing.

This only happens when I run with -O passed to the compiler. A debug build prints the expected thing.

It can be reproduced with the following files:

# main.rs
fn main() {
    let a: i32 = 7;
    test(Some(a));
}

fn test(a: Option<i32>) {
    let mut a_ptr = std::ptr::null();
    if let Some(v) = a {
        a_ptr = &v as _;
    };
    unsafe {
        cfunc(a_ptr);
    };
}

extern "C" {
    pub fn cfunc(a: *const i32);
}

# func.c
#include <stdint.h>
#include <stdio.h>

void cfunc(const int32_t *b)
{
    if (b)
        printf("%d\n", *b);
    else
        printf("Null\n");
}
#!/usr/bin/env bash
# build.sh
gcc -c func.c -o func.o
ar rcs libfunc.a func.o
rustc main.rs -L . -lfunc $1

Then in a shell:

$ ./build.sh ; ./main
7 
$ ./build.sh -O; ./main
22014

Without the -O flag, I get 7 which is expected.
With the -O flag, I get nonsense. It is actually a different number each time I run it.

If I take away the null possibility and just pass &a.unwrap() as *const i32 to cfunc() then it works fine. So it seems like somehow the fact that the pointer is sometimes null is screwing things up. It seems I'm somehow getting a pointer to stale data, though not sure how, and why it only shows up in the optimized build. I get no compiler warnings or anything.

Does anyone know what is going wrong here?

You move out of the option when you match on it, so the value to which you take the address is destroyed by the time you pass the pointer to the C function.

Try matching on option.as_ref() instead:

if let Some(v) = a.as_ref() {
    a_ptr = &v;
}

By the way, you don't need either the explicit cast or the mutable state. Rust is an expression language. You can just write

let a_ptr: *const _ = match a.as_ref() {
    Some(v) => v,
    None => std::ptr::null(),
};
3 Likes

This line looks a bit suspect. The if let Some(v) = a bit will deconstruct the option by value and bind the integer to a new local variable on the stack called v. I'm guessing when you enable optimisations the optimiser sees that the temporary v variable won't outlive the if-let block so the pointer we assign a_ptr to will be dangling and can be set to whatever it wants (most probably left uninitialized).

A better way to write it would be like this:

if let Some(v) = &a {
  a_ptr = v as *const i32;
}

Or even better

let a_ptr = a.as_ref()
    .map(|v: &i32| v as *const i32)
    .unwrap_or(std::ptr::null());
1 Like

Thank you both!! That was indeed the issue.

Alternatively it's possible to declare API function as follows.

extern "C" {
    pub fn cfunc(a: Option<&i32>);
}

This is compatible with *const i32, and will result in somewhat friendlier API with Rust, allowing you to do cfunc(a.as_ref()).

4 Likes

Ah, I did not know that! Yes, that would be much nicer.

However, I'm using Bindgen to create the function declarations, and I'm not sure if there is a way to get it to create the declaration in that form. It would need to know somehow that the pointer is meant to be optional.

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.