Option<T> is not FFI-safe

Hi, all guys. I'm new to Rust.

I have a function which its prototype is like:
pub fn fuzzy_match_v1(case_sensitive: bool, normalize: bool, forward: bool, text: &Rune, pattern: &Rune, with_pos: bool) -> Option<MatchResult>

#[derive(Debug)]
pub struct MatchResult {
    start: usize,
    end: usize,
    score: i32,
    pos: Vec<usize>,
}

pub struct Rune {
    v: Vec<char>,
}

When I add #[no_mangle] to the fuzzy_match_v1, rustc complains about the Option<MatchResult> is not FFI-safe.

diff --git a/src/lib.rs b/src/lib.rs
index 2f03dde..d84a2ff 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -290,7 +290,8 @@ fn calculate_score(case_sensitive: bool, normalize: bool, text: &Rune, pattern:
     (score, pos)
 }
 
-pub fn fuzzy_match_v1(case_sensitive: bool, normalize: bool, forward: bool, text: &Rune, pattern: &Rune, with_pos: bool) -> Option<MatchResult> {
+#[no_mangle]
+pub extern "C" fn fuzzy_match_v1(case_sensitive: bool, normalize: bool, forward: bool, text: &Rune, pattern: &Rune, with_pos: bool) -> Option<MatchResult> {
     if pattern.v.is_empty() {
         return Some(MatchResult {
             start: 0,
$ cargo build
warning: `extern` fn uses type `Option<MatchResult>`, which is not FFI-safe
   --> src/lib.rs:294:136
    |
294 | ...l, text: &Rune, pattern: &Rune, with_pos: bool) -> Option<MatchResult> {
    |                                                       ^^^^^^^^^^^^^^^^^^^ not FFI-safe
    |
    = note: `#[warn(improper_ctypes_definitions)]` on by default
    = help: consider adding a `#[repr(C)]`, `#[repr(transparent)]`, or integer `#[repr(...)]` attribute to this enum
    = note: enum has no representation hint

warning: `fzf-fuzzy-match-rs` (lib) generated 1 warning
    Finished dev [unoptimized + debuginfo] target(s) in 0.01s

I certainly misunderstood something, anyone can help with this?

Basically, I wanted to export the fuzzy_match_v1 to the corresponding C function signature so I can call this Rust library from C/C++.

C has no concept of enums, so it's not directly possible to expose Option<Anything> to FFI – what would the signature at the C side look like, after all?

You will need to rewrite the function in a way that is compatible with C, i.e. it should only contain primitives and #[repr(C)] structs. You can simulate an Option return type using an out parameter that will be filled in by the function sometimes, and a bool return value that tells whether it was in fact filled in.

3 Likes

The C signature should somewhat like:

MatchResult* fuzzy_match_v1_c(bool case_sensitive, bool normalize, bool forward, const char *text, const char *pattern, bool with_pos);

BTW, can I use #[repr(C)] for my MatchResult and Rune struct, since there is a Vec<usize>.

Then you need some pointer, e.g. Option<Box<MatchResult>>.

No, and you cannot represent them in C. Does the C code accesses the fields or only pass the pointer around?

1 Like

I need to access its fields(i.e. start, end, score, pos)

If so, what's the best way to make this Rust fuzzy_match_v1 function compatible with the C function?

Like the Vec<char>, essentially, it's a C uint32_t array with a known length.

Vec is not an array, more like a dynamic array. If this is an array in C, it should be an array in Rust too. If you will show us the C definition we will be able to tell how the Rust definition should look like.

Well, but that's the thing – MatchResult * is a pointer, and MatchResult isn't. That's a type mismatch right there.

What I'm suggesting is you add a pointer argument and indicate via the return type whether it was written to:

bool fuzzy_match_v1_c(
    bool case_sensitive,
    bool normalize,
    bool forward,
    const char *text,
    const char *pattern,
    bool with_pos,
    MatchResult *out
);

which is modeled in Rust as:

fn fuzzy_match_v1_c(
    case_sensitive: bool,
    normalize: bool,
    forward: bool,
    text: *const c_char,
    pattern: *const c_char,
    with_pos: bool,
    out: &mut MatchResult,
) -> bool;
1 Like

Vec is not an array, more like a dynamic array.

@chrefr, thanks for rectifying me.

The MatchResult in C definition:

struct MatchResult {
  uint32_t start;
  uint32_t end;
  uint32_t score;
  uint32_t pos_len;
  /**
   * Length is denoted by pos_len, if pos_len = 0, this should be NULL.
   * Must be freed by the caller.
   */
  uint32_t *pos;
}

Then the Rust definition should be:

#[repr(C)]
struct MatchResult {
    start: u32,
    end: u32,
    score: u32,
    pos_len: u32,
    pos: *mut u32,
}
4 Likes

Hi, @H2CO3, for the out: &mut MatchResult, how could I check for the nullability of the out pointer?

Rust references are never null.

But why would you need to check it in the first place? It represents an output parameter, which is only written to — the caller must ensure that it points somewhere sensible.

Do you mean when I call fuzzy_match_v1_c(..., NULL) the Rust FFI will automatically check the nullability?
What happens when I pass a NULL to fuzzy_match_v1_c?

But why would you need to check it in the first place?
I need to check if the user passed a NULL, in which case I should return false immediately.

    if (out as *mut MatchResultC).is_null() {
        return false;
    }

so (out as *mut MatchResultC).is_null() will always yield a false value?

The "user" here is the C code, not the Rust code, right? In this case, you should use out: *mut MatchResult instead of reference, since references can't point to nowhere, and passing an invalid pointer where reference is expected is UB.

It won't. There are no runtime checks in FFI. FFI isn't a thing that does something. It's merely a set of conventions that allow Rust to call C code and generate/expose code as if it were were written in C.

UB. You don't pass a NULL pointer when the signature expects a reference, since Rust references aren't allowed to be null.

Yes.

However, I'm increasingly confused about what you are trying to do. Please formulate clearly what Rust and C code you want to write, what their role is, what they should do, who will call whom.

Hi, all guys. I found that out: &mut MatchResult can be NULL if as *mut MatchResult.

int main(void) {
    bool ok = fuzzy_match_v1_c(false, false, true, "foo bar baz", "fbb", true, NULL);
    printf("%d\n", ok);
    return 0;
}

and the (out as *mut MatchResultC).is_null() yield true.

Yeah. "It's merely a set of conventions that allow Rust to call C code and generate/expose code as if it were written in C."

If you don't check the nullability, it'll SEGFAULT.

$ LD_LIBRARY_PATH=./target/debug ./a.out
OUT is null!
[1]    564174 segmentation fault (core dumped)  LD_LIBRARY_PATH=./target/debug ./a.out

No, that is incorrect. If you somehow create a reference that ends up being null, then you have undefined behavior.

If you need to allow null, then use Option<&mut MatchResult> (or *mut MatchResult, but the former is preferred unless a raw pointer is specifically needed, as it can be dereferenced without unsafe).

1 Like

I actually don't allow the NULL, but C user may pass NULL incorrectly.
So I'd better return false earlier if parameters are invalid.

The important thing is that if you write &mut _ on the Rust side and the C side supplies NULL, it is UB. There is no longer any guarantee to the programs behavior, at all. This is not a condition that you can detect, because the language just says it can't happen, and when you use unsafe it's your job to ensure that this remains the case.

(If you turn on optimizations, it is quite likely that the null check would be optimized out, because it's guaranteed to never happen.)

If you want to be resilient to a null pointer being provided, you must write the Rust signature to accept *mut or Option<&mut>.

5 Likes

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.