Search code examples
javarustcrashjnr

Why does Rust native library randomly crash when calling function?


In Rust, I have a function that compresses an array:

fn create_bitvec(data: Box<[u8]>) -> [u8; 49152] {
    let mut bit_vector = [0u8; 49152];
    for (index, &value) in data.iter().enumerate() {
        let byte_index = (index * 4) / 8;
        let bit_index = (index * 4) % 8;
        let value_mask = 0b00001111 << bit_index;
        let shifted_value = (value << bit_index) & value_mask;
        bit_vector[byte_index] &= !value_mask;
        bit_vector[byte_index] |= shifted_value;
    }
    debug!("made it here. {:?}", bit_vector);
    bit_vector
}

(sorry if the implementation of the function is bad, but that's not the specific problem here).

The function is used like this:

pub fn create(data: Box<[u8]>) -> Chunk {
    assert_eq!(data.len(), FULL, "Data length doesn't match!");
    let bitvec = Chunk::create_bitvec(data);
    debug!("Got here 1!");
    let c = Chunk {
        data: Chunk::create_rle(bitvec),
    };
    debug!("Got here 2: {:?} {}!", c.data, c.data.len());
    c
}

This program is used as a native library for Java. The struct is not directly used, but only some crucial functions are exposed using extern "C" fn from lib.rs. When calling create from rust as part of a unit test, everything works as expected, but when used from the JVM, the program crashes with no additional panic/debug info, with exit code -1073740940 (0xC0000374). The really confusing part that I simply cannot wrap my head around is that made it here gets logged just fine, with the correct array, but the program crashes before Got here 1 can be logged. Here are some things I have already tried:

  • Verifying the array length

  • Verifying the data fits under the u8 type

  • Testing the code from only Rust

I suspect the issue may have something to do with memory, but I am not sure, especially since I passed Xmx8G to the JVM.

I am using jnr-ffi for the natives.

Here is the relevant rust code from lib.rs:

static CHUNK_STATE: Mutex<Cell<Option<ChunkManager>>> = Mutex::new(Cell::new(None));
#[no_mangle]
pub extern "C" fn chunk_build(x: i64, y: i64, arr: *const u8) {
    info!("Building chunk: {}, {}", x, y);
    unsafe {
        CHUNK_STATE.lock().get_mut().as_mut().expect("Not initialized!").build((x, y), Box::from_raw(slice::from_raw_parts_mut(arr as *mut u8, WIDTH * WIDTH * HEIGHT)));
    }
}

And its usage from Java:

public interface NativeLib {
    void chunk_build(int x, int y, byte[] arr);
}
import jnr.ffi.LibraryLoader;

public class Natives {
    public static NativeLib INSTANCE;

    public static void init() {
        INSTANCE = LibraryLoader.create(NativeLib.class).load("C:\\Users\\*\\*\\*\\nativelib\\target\\release\\pathlib.dll");
        INSTANCE.init();
    }
}

Solution

  • You should not use Box to handle memory you got from the Java side. Box will attempt to deallocate the memory when it goes out of scope, but that will encounter errors since the memory was not allocated by Box initially.

    Instead use the slice only:

    fn create_bitvec(data: &[u8]) -> [u8; 49152] {
        // ...
    }
    
    pub fn create(data: &[u8]) -> Chunk {
        // ...
    }
    
    .build((x, y), slice::from_raw_parts_mut(arr as *mut u8, WIDTH * WIDTH * HEIGHT));
    

    And any other code you omitted that uses it. The implementations within the functions should not change (much) since Box is fairly transparent.