jmegaffin
jmegaffin

Reputation: 1172

Move semantics in Rust

I'm wrapping a C library in Rust, and many of its functions take parameters by pointers to structs, which themselves often have pointers to other structs. In the interest of reducing overhead, I'd like to provide the ability to cache the results of marshaling the Rust data into the C structs.

Here's an example of how the C library might expect some parameters:

#[repr(C)]
struct Foo {
    x: i32,
    y: f32
}

#[repr(C)]
struct Bar {
    p_foo: *const Foo,
    z: bool
}

And how I'd imagine an owning, "cached" version would look:

struct Cached {
    foo: Option<Foo>,
    bar: Bar
}

The p_foo field of bar would be constructed to point to Some value within foo, or a null pointer if there was None.

The issue, here, of course, is that if a value of Cached was to be moved, a straight memcpy would be inappropriate and bar.p_foo would additionally need to be redirected. This would be easy to ensure in C++, with its definable move semantics, but does Rust offer a solution besides "don't set bar.p_foo until it's used"? While it would certainly work to do it that way, I don't imagine that these cached values will be moved more than (or even close to the frequency that) they are reused, and there is a bit of work involved to set up these pointers, especially if the nesting/chaining is deep/long. I'd also rather not Box the substructures up on the heap.


To clarify, here's what I can write in C++, which I would like to replicate in Rust:

struct Foo {
    int x;
    float y;
};

struct Bar {
    Foo const*pFoo;
    bool z;
};

// bear with me while I conjure up a Maybe for C++
class Cached {
public:
    // would have appropriate copy constructor/assignment

    Cached(Cached &&other) {
        m_foo = other.m_foo;
        m_bar = other.m_bar;

        if(m_foo.isJust()) {
            m_bar.pFoo = &m_foo.value();
        } // else already nullptr
    }

    // similar move assignment

private:
    Maybe<Foo> m_foo;
    Bar m_bar;
};

Upvotes: 2

Views: 427

Answers (1)

oli_obk
oli_obk

Reputation: 31283

The Rust-equivalent would be to not use raw pointers, as raw pointers are there for implementing our safe datastructures, not for implementing normal datastructures.

#[repr(C)]
struct Foo {
    x: i32,
    y: f32
}

#[repr(C)]
struct Bar {
    p_foo: Option<Box<Foo>>,
    z: bool
}

An Option<Box<T>> is guaranteed to be exactly equivalent (in bits in memory) to a *const T, as long as T is a type and not a trait. The only difference is that it's safe to use within Rust.

This way you don't even need a Cached struct anymore, but can directly pass around the Bar object.


I'd also rather not Box the substructures up on the heap.

Then I suggest you don't keep a Bar object around, and instead conjure it up whenever you need to pass one to C:

#[repr(C)]
struct Foo {
    x: i32,
    y: f32
}

#[repr(C)]
struct Bar<'a> {
    p_foo: Option<&'a Foo>,
    z: bool
}

struct Cached {
    foo: Option<Foo>,
    z: bool,
}

impl Cached {
    fn bar<'a>(&'a self) -> Bar<'a> {
        Bar {
            p_foo: self.foo.as_ref(),
            z: self.z,
        }
    }
}

there is a bit of work involved to set up these pointers, especially if the nesting/chaining is deep/long.

That sounds a lot like premature optimization. Don't optimize where you haven't benchmarked.

Upvotes: 1

Related Questions