4

There is a Rust library that provides three functions:

  • a constructor for a vector of floats
  • a function to push a number to the vector
  • a function that sums over the vector

Like this:

#[no_mangle]
pub extern "C" fn initialize_vector() -> *mut Vec<f32> {
    Box::into_raw(Box::new(Vec::<f32>::new()))
}

#[no_mangle]
pub extern "C" fn push_to_vector(ptr: *mut Vec<f32>, x: f32) -> *mut Vec<f32> {
    if ptr.is_null() { return ptr; }
    let vector_box = unsafe { Box::from_raw(ptr) };
    let mut example_vector = *vector_box;
    example_vector.push(x);
    Box::into_raw(Box::new(example_vector))
}

#[no_mangle]
pub extern "C" fn sum_vector(ptr: *mut Vec<f32>) -> f32 {
    if ptr.is_null() { return 0.0; }
    let vector_box = unsafe { Box::from_raw(ptr) };
    let example_vector = *vector_box;
    return example_vector.iter().sum();
}

initialize_vector creates a Vec and wraps it in a Box, returning the raw pointer to the caller. The caller is now responsible for appropriately freeing the allocation.

push_to_vector takes ownership of the Box<Vec<f32>>, moves the Vec out of the Box, freeing the memory allocated by Box in the process. It then adds an item to the vector, creates a brand new Box (with associated allocation) around the Vec and returns it.

sum_vector also takes ownership of the Box<Vec<f32>>, moves the Vec out of the Box and frees the memory allocation. Since ownership of the Vec doesn't leave the function, Rust will automatically free the memory associated with the Vec when the function exits.

The C# code that uses the library could look like this:

using System;
using System.Runtime.InteropServices;

internal class Blabla
{
    [DllImport("Example")]
    unsafe static public extern IntPtr initialize_vector();

    [DllImport("Example")]
    unsafe static public extern IntPtr push_to_vector(IntPtr ptr, float x);

    [DllImport("Example")]
    unsafe static public extern float sum_vector(IntPtr ptr);

    static public unsafe void Main()
    {
        IntPtr vec_ptr = initialize_vector();
        vec_ptr = push_to_vector(vec_ptr, (float) 2.2);
        vec_ptr = push_to_vector(vec_ptr, (float) 3.3);
        float result = sum_vector(vec_ptr);
        // is the memory allocated for the vector in Rust freed right now?
        Console.WriteLine(string.Format("Result: {0}", result));
        Console.ReadLine();
    }
}

In general, it is recommended to use SafeHandle to finalize pointers returned by DLL functions, see e.g. this question or this example. I understand that the purpose of SafeHandle is mostly to call a destructor in certain cases, when something unplanned happens.

Since it is not necessary to free the Rust vector after the sum_vector function has been called, is it still advisable to use SafeHandle in the given situation, and if yes - how? Or can I just leave the code as it is and everything is fine?

Shepmaster
  • 388,571
  • 95
  • 1,107
  • 1,366
Amos Egel
  • 937
  • 10
  • 24
  • 1
    Your question is reasonable, but I'd say the code example provided doesn't make sense. There's no *reason* to take ownership of the `Box>` in either `push_to_vector` or `sum_vector`. If you simply changed these functions to *borrow* the data and restored the destructor, the code would be more performant (less unneeded (de-)allocation) and simpler. Is this just an artifact of creating a MCVE? – Shepmaster Sep 25 '18 at 15:18
  • @Shepmaster Thanks for the hint! The example is pretty similar to what I actually do, except that it is not floats but structs and that the analog of `sum_vector` does some heavy calculation. Thus, performance of allocation / deallocation is not important. But I could still apply the changes that you suggest (i.e., only borrow the vector and finally use a destructor) in order to manage the pointer in C# with the `SafeHandle` class. – Amos Egel Sep 26 '18 at 07:24
  • @Shepmaster And how can I even push to the vector without taking ownership? – Amos Egel Sep 26 '18 at 12:53
  • 1
    `Vec::push` only takes a `&mut Self` and `iter` only needs a `&Self`. You [can dereference and re-reference](https://play.rust-lang.org/?gist=6048282d021a31ca61f5e43157ee1366&version=stable&mode=debug&edition=2015). – Shepmaster Sep 26 '18 at 13:21
  • *performance of allocation / deallocation is not important* — I would disagree, but only benchmarking can say for sure. My gut says that de-allocating and re-allocating memory for *every item you push* is extremely wasteful. – Shepmaster Sep 26 '18 at 14:51

0 Answers0