0
votes

I need a binary tree data structure in C, and there is already exist a binary tree implementation in Rust. So I decided to wrap that.

I made a C-compatible struct contains raw pointer to BTreeMap and add some methods that take a pointer to my wrapper struct.

The problem is in module test. The test is passed if I using my methods only, but whenever I put the println! macro between method calls, The test failed.

use std::collections::BTreeMap;

#[repr(C)]
pub struct my_btree {
    btree: *mut BTreeMap<u64, u64>,
}

#[no_mangle]
pub extern "C" fn my_btree_new() -> *mut my_btree {
    let boxed = Box::new(BTreeMap::<u64, u64>::new());
    let mut ret = my_btree {
        btree: Box::into_raw(boxed),
    };

    &mut ret as *mut my_btree
}

#[no_mangle]
pub extern "C" fn my_btree_insert(
    btree: *mut my_btree,
    key: u64,
    val: u64,
) -> bool {
    unsafe {
        let mut boxed = Box::from_raw((*btree).btree);
        let contains = (*boxed).contains_key(&key);
        if contains {
            return false;
        }

        (*boxed).insert(key, val);
        println!("{:?}", boxed);
        Box::into_raw(boxed);

        return true;
    }
}

#[no_mangle]
pub extern "C" fn my_btree_contains(btree: *mut my_btree, key: u64) -> bool {
    unsafe {
        let boxed = Box::from_raw((*btree).btree);
        println!("{:?}", boxed);
        let ret = (*boxed).contains_key(&key);

        Box::into_raw(boxed);

        ret
    }
}

#[no_mangle]
pub extern "C" fn my_btree_free(btree: *mut my_btree) {
    unsafe {
        let _boxed = Box::from_raw((*btree).btree);
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    
    #[test]
    fn insert() {
        let map = my_btree_new();
        my_btree_insert(map, 1, 1);
        my_btree_insert(map, 2, 2);
        my_btree_insert(map, 3, 30);
        let err = my_btree_contains(map, 1);
        assert_eq!(err, true);
        println!("???"); // If this line commented out, then test success without error.
        my_btree_free(map);
    }
}

when I run the test with below command,

$ cargo test -- --nocapture

In my terminal,

running 1 test
{1: 1}
{1: 1, 2: 2}
{1: 1, 2: 2, 3: 30}
{1: 1, 2: 2, 3: 30}
???
error: test failed, to rerun pass '--lib'

But if I comment out the println!("???"); then test pass without any error.

And if I put println between my_btree_insert calls, it failed in next call. It's weird. Why this happened?

1
welcome to undefined behavior, take a seat.Stargateur

1 Answers

1
votes

You've got several issues.

  1. In my_btree_new, you construct an instance of my_btree on the stack (ie. local to the function) and then return a pointer to it.

  2. In my_btree_insert, you take your pointer, then construct a Box around it. Before returning, you deconstruct the Box so that the item won't be freed - but you also have an early return path that does not deconstruct the Box. Your test case does not exercise that code path, but I expect it would crash if it did.

Why does it only crash when you insert the println!? Simple - the extra function call(s) that are generated trash the area of the stack containing the my_btree.

Here are a few suggestions for fixing it up:

  • You do not really need the wrapper struct (at least for this basic example). But if you are going to have one, the whole struct should be on the heap, not just the BTree structure.

  • You don't need to box/unbox the pointer in every method; it is not adding value and is just increasing the chances you will forget to rebox it in some code path, resulting in a double-free crash. Only re-box it in the my_btree_free function.

  • You have made all these functions safe with the code inside wrapped in an unsafe block. That's not really correct - the compiler cannot verify that the pointer being supplied is correct, therefore the function is not safe (or putting it another way, a safe function should not crash regardless of the arguments you supply).

Here is a version that works:

use std::collections::BTreeMap;

#[repr(C)]
pub struct my_btree {
    btree: BTreeMap<u64, u64>,
}

#[no_mangle]
pub extern "C" fn my_btree_new() -> *mut my_btree {
    let boxed = Box::new(my_btree { btree: BTreeMap::<u64, u64>::new() } );
    Box::into_raw(boxed)
}

#[no_mangle]
pub unsafe extern "C" fn my_btree_insert(
    btree: *mut my_btree,
    key: u64,
    val: u64,
) -> bool {
    let contains = (*btree).btree.contains_key(&key);
    if contains {
        return false;
    }
    (*btree).btree.insert(key, val);

    return true;
}

#[no_mangle]
pub unsafe extern "C" fn my_btree_contains(btree: *mut my_btree, key: u64) -> bool {
    (*btree).btree.contains_key(&key)
}

#[no_mangle]
pub unsafe extern "C" fn my_btree_free(btree: *mut my_btree) {
    let _boxed = Box::from_raw(btree);
}

#[cfg(test)]
mod tests {
    use super::*;
    
    #[test]
    fn insert() {
        let map = my_btree_new();
        unsafe {
            my_btree_insert(map, 1, 1);
            my_btree_insert(map, 2, 2);
            my_btree_insert(map, 3, 30);
            let err = my_btree_contains(map, 1);
            assert_eq!(err, true);
            println!("???"); // If this line commented out, then test success without error.
            my_btree_free(map);
        }
    }
}