Search code examples
rust

STATUS_HEAP_CORRUPTION after std::slice::from_raw_parts_mut


I am trying to wrap rustfft in functions that I can use via bindings from another language. In order to achieve this, I need two functions: one to create the plan, one to run the fft. To simplify data exchange and binding generation, I want one of the parameters to be a list of f64. I found that there's at least some performance penalty when copying around input and output data, so I believe it might be beneficial to let rustfft run on the passed data in-place.

Here's my code so far. It gives an error with a STATUS_HEAP_CORRUPTION when main finishes, but I could not find out why I get this error and what I did wrong.

use rustfft::{Fft, FftPlanner};

fn main() {
    const SIZE: usize = 8;
    let fft = create_fft_plan(SIZE);

    let mut buffer_raw = vec![0.0f64; SIZE * 2];
    buffer_raw[0] = 1.0;
    println!("Raw buffer before fft: {:?}", buffer_raw);
    run_fft(&fft, &mut buffer_raw);
    println!("Raw buffer after fft: {:?}", buffer_raw);
}

pub fn create_fft_plan(size: usize) -> std::sync::Arc<dyn Fft<f64>> {
    let mut planner = FftPlanner::new();
    let fft = planner.plan_fft_forward(size);
    fft
}

pub fn run_fft(fft: &std::sync::Arc<dyn Fft<f64>>, buffer: &mut [f64]) {
    // We want to modify buffer in-place for performance reasons: This function is likely 
    // to be called very often with big buffers. Vec<f64> and Vec<Complex<f64>> have the same
    // layout in memory as far as I found out so far.
    unsafe {
        let buffer_slice = workaround_transmute_mut(buffer);
        fft.process(buffer_slice);
    };
}

// This function was taken from rustfft-6.2.0\src\array_utils.rs
unsafe fn workaround_transmute_mut<T, U>(slice: &mut [T]) -> &mut [U] {
    let ptr = slice.as_mut_ptr() as *mut U;
    let len = slice.len();
    std::slice::from_raw_parts_mut(ptr, len)
}

The output that I get shows a valid fft result:

Compiling rust_fft_test v0.1.0 (C:\Users\Markus\dev\rust\rust_fft_test) Finished dev profile [unoptimized + debuginfo] target(s) in 1.45s Running target\debug\rust_fft_test.exe Raw buffer before fft: [1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0] Raw buffer after fft: [1.0, 0.0, 1.0, 0.0, 1.0, 0.0, 1.0, 0.0, 1.0, 0.0, 1.0, 0.0, 1.0, 0.0, 1.0, 0.0] error: process didn't exit successfully: target\debug\rust_fft_test.exe (exit code: 0xc0000374, STATUS_HEAP_CORRUPTION)

  • The terminal process "C:\Users\Markus.cargo\bin\cargo.exe 'run', '--package', 'rust_fft_test', '--bin', 'rust_fft_test'" terminated with exit code: -1073740940. * Terminal will be reused by tasks, press any key to close it.

Solution

  • Vec<f64> and Vec<Complex<f64>> have the same layout in memory

    This is wrong. Complex is indeed #[repr(C)] so its layout is guaranteed, but it contains two f64s, so it is double the size of one.

    You need to adjust the length:

    unsafe fn workaround_transmute_mut(slice: &mut [f64]) -> &mut [Complex<f64>] {
        let ptr = slice.as_mut_ptr() as *mut U;
        let len = slice.len() / 2;
        std::slice::from_raw_parts_mut(ptr, len)
    }
    

    This will silently skip the last element if the length is odd.

    Or better, use bytemuck to do it safely. num-complex has integration with it, but you need to enable the bytemuck feature:

    num-complex = { version = "0.4.6", features = ["bytemuck"] }
    bytemuck = "1.16.1"
    
    fn workaround_transmute_mut(slice: &mut [f64]) -> &mut [Complex<f64>] {
        bytemuck::cast_slice_mut(slice)
    }
    

    This will panic if the length is odd. There is also a fallible variant.