2

In a mixture of Rust and C code for a microcontroller, I have a Rust data structure that I have to make known to the C part of my program. I got this working.

pub struct SerialPort {
    // ... some attributes ...
}

I tried to refactor the Rust code and wanted to generate the Rust structure in a function that also registers the callbacks in my C Code.

pub fn setup_new_port() -> SerialPort {
    let port = SerialPort::new();
    port.register_with_c_code();
    port
}

This doesn't work because Rust will move the content of the memory of my variable of type SerialPort to a different location when the function returns. The addresses registered in the C code point to the (now freed) stackframe of setup_new_port() while it got moved to the stack frame of the caller, so my C code will access invalid addresses.

Is there any trait that I can implement that gets notified when a move happens? I could adjust the addresses registered in my C code in the implementation of the trait.

I know that I could prevent the move from happening by allocating my structure on the heap, but I would like to avoid this as the code runs on a microcontroller and dynamic memory allocation might not always be present.

Shepmaster
  • 388,571
  • 95
  • 1,107
  • 1,366
Matthias Wimmer
  • 3,789
  • 2
  • 22
  • 41
  • Do you have a heap? – Boiethios Jul 26 '18 at 09:16
  • @Boiethios On the system I'm currenty implementing this I do. But that's not always the case, so I'd prefer to prevent using the heap. (The structure isn't big and it doesn't have to be fast. So moving it is okay in my usecase.) – Matthias Wimmer Jul 26 '18 at 09:18
  • 1
    To be clear, this is *exactly* what C does. It's only because you are using FFI and raw pointers that the problem even occurs in Rust. If your FFI function used lifetimes (which doesn't make sense), the Rust compiler would [*prevent* this code from compiling](https://stackoverflow.com/q/32682876/155423). – Shepmaster Jul 26 '18 at 13:15
  • @Shepmaster Thanks for the clarification, but I wasn't complaining Rust. It actually make perfect sense. But what do you mean by “exactly what C does”? I've never seen move semantics in C. – Matthias Wimmer Jul 26 '18 at 17:17
  • In C, you'd be creating your `SerialPort` struct in the stack frame of `setup_new_port`, passing a reference to it to `register_with_c_code`, and then immediately invalidating the reference by exiting the function. That's what's happening here because you are using raw pointers. "Move semantics" exist in C and C++, Rust just allows the compiler to help you with them. – Shepmaster Jul 26 '18 at 17:22
  • @Shepmaster I see what you mean and compare. But in C I think its more obvious for the programmer as he will think more of returning a copy of the data. (Or its me having a better understanding of what the compiler produces in C.) You might think a lot in how the Rust compiler does things on the machine level. For a developer new at Rust just using the language the move semantics feels more like its still the same variable. So at first sight the changed memory address might be a surprise. I think at this point also the name “move semantics” might suggest a wrong understanding at first. – Matthias Wimmer Jul 26 '18 at 17:28

3 Answers3

9

No, by design.

Rust was specifically designed without so-called move constructors, in Rust a move is a bitwise copy, allowing many optimizations:

  • such as the use of C memcpy and realloc,
  • optimizations based on the fact that no panic! can be called during a move,
  • ...

Actually, there are proposals in C++ to retrofit such a design (is_relocatable), and performance is the main driver.


So what?

Do it like Mutex do! That is, Box whatever should not move, and isolate it in an opaque struct so that nobody can move the content out of the box.

If heap allocation is not available... then you may want to look into PinMut, or otherwise borrow the structure. What is borrowed cannot be moved.

kennytm
  • 510,854
  • 105
  • 1,084
  • 1,005
Matthieu M.
  • 287,565
  • 48
  • 449
  • 722
  • Thank you for this detailed information with a lot of helpful pointers. I guess I go with using the heap for the moment. Everything else seems to make the code more complicated while not being necessary so far. – Matthias Wimmer Jul 26 '18 at 09:36
2

The only solution I see (I do not says that it is the only solution) is to have an arena in the stack, or another kind of placeholder:

#[derive(Default)]
pub struct SerialPort {
    _dummy: i32,
}

pub fn setup_new_port(placeholder: &mut Option<SerialPort>) -> &SerialPort {
    let port = SerialPort::default();
    std::mem::replace(placeholder, Some(port));
    let port = placeholder.as_ref().unwrap();
    println!("address: {:p}", port);
    //port.register_with_c_code();
    port
}

fn main() {
    let mut placeholder = None;

    let port = setup_new_port(&mut placeholder);
    println!("address: {:p}", port);
}

The two println! lines print the same value.

I searched in the internet to find a crate that implements an arena in the stack but found nothing so far.

Boiethios
  • 38,438
  • 19
  • 134
  • 183
1

I would split it up. I'd create an instance of the Serialport somewhere (on the stack) and afterwards call register_with_c_code(). Also I would impl Drop for Serialport, so the c_code would be detached, when the Serialport goes out of scope.

For your Serialport interface, I would not accept any move, but only (mutable) references.

hellow
  • 12,430
  • 7
  • 56
  • 79
  • `mem::forget` can consume a stack-allocated `Serialport` without calling `drop`, which may lead to memory unsafety. – trent Jul 26 '18 at 14:05