Holding a mutable context and working with its contents

Hi

I have this minimal example in the playground
https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=e5f5583808701cbc5fbcbaf4cf794999

I am starting with Rust and I am trying to -somehow- replicate the design of some of my libraries in C where I have a global context pointer and I maintain all the needed data hanging from there, C structs with pointers to other C structs and so on.

When I try to code the same concept in Rust I get the error

cannot borrow `*x` as mutable more than once at a time

So the question is, how would you face this issue? I need to keep lot of data referenced by a pointer because at a later stage of the project I will export a C interface to C users and they will be only working with an opaque handle

Thanks

You can't. What does prevent the work function from modifying the Vec items and therefore invalidate the for loop? You can work around this by passing the mult directly, e.g. work(x.mult, item).

Your code still looks very C-ish. I think you would greatly benefit from reading the Rust book.

However, having a global, mutable state is hard to do correct and since Rust is preventing you from doing crap, it is even harder to get global, mutable state in Rust. But it seems that you want to pass your Ctx around so that should be fine.
Here I have a more 'rusty' version of your example.

2 Likes

@hellow The example is a mcve to make sure I transmit the point correctly. The actual implementation of the library I'm working on is much more complex with probably thousands of functions that work over the same data. That is why that workaround is not applicable for all the use cases. Thanks anyway

I just read the rust book, but as I'm doing actual coding with this language I'm facing new issues. I think this issue is the most interesting one I found so far because it would require a change of paradigm, or somehow a go/no-go for rust in our company. We have a huge codebase that relies on APIs that are based on opaque pointers

To explain myself better, I have no global state whatsoever (global as in global variable) but pointers to heap structures that are passed back an forth.

I checked your code, and it seems like it is more similar like this, and still has the same issue with the mutability

Thanks

Okay I'm not an expert for FFI stories. What I imagine could work is passing the structs around in reference-counted wrappers (Arc<Mutex<_>> or Rc<RefCell<_>> if single threaded) and then passing them as pointers through the FFI boundaries.

1 Like

You may need to write things a little differently to the way you normally would in C, but I don't see why you wouldn't be able to maintain (roughly) the same architecture.

The way you use opaque pointers in C is roughly analogous to the concept of an "object" that has methods. i.e. you may not necessarily be able to see the object's internals, but there's a well defined set of behaviour that can be used with the object. This maps quite nicely to a Rust struct (or enum) which has methods that mutate its internal properties.

When writing Rust code, something to keep in the forefront of your mind is ownership. So you'd probably make the business objects (e.g. Ctx) own the data they work on, instead of just retaining a mutable reference to it.

Something else you may find useful is the compiler is smart enough to know when you're mutably borrowing distinct fields. That means something like this is perfectly legal (and would probably help with your example):

#[derive(Debug)]
struct Ctx {
    mult: i32,
    items: Vec<i32>,
}

fn main() {
    let mut ctx = Ctx {
        mult: 100,
        items: vec![1, 2, 3],
    };

    let mult = &mut ctx.mult;
    let items = &mut ctx.items;

    for item in items {
        *item *= *mult;
    }

    println!("{:?}", ctx);
}

It's a little hard to explain without concrete examples (e.g. looking at hundreds of lines from your codebase and discussing how you'd do the same thing in Rust), but as long as your API follows best practices (loose coupling, tests, etc.) and doesn't go too crazy with everything having mutable pointers to everything else, it's entirely possible to use Rust with your codebase. At the small scale it may require approaching things differently, but I've found designing the architecture for a Rust project is just like any other language.

As you go through and you find something which is hard to express in Rust, post to this forum and we'll help you figure it out :slightly_smiling_face:

4 Likes

Well...

I would still first and foremost try to avoid this sharing in rust code in the first place. Just because the C API requires everything to be in a single Context doesn't mean the Rust code should:

fn work(i: &mut i32, mult: i32) {
    *i = *i * mult;
}

fn proc(items: &mut [i32], mult: i32) {
    for item in items.iter_mut() {
        work(item, mult);
    }
}

With these, you can still expose a C API that involves a single Ctx:

mod c_api {
    #[derive(Debug)]
    pub struct Ctx<'a> {
        pub mult: i32,
        pub items: &'a mut Vec<i32>,
    }
    
    pub extern "C" fn work(x: *mut Ctx<'_>, i: *mut i32) {
        let x = unsafe { x.as_mut().unwrap() };
        let i = unsafe { i.as_mut().unwrap() };
        super::work(i, x.mult);
    }
    
    pub extern "C" fn proc(x: *mut Ctx<'_>) {
        let x = unsafe { x.as_mut().unwrap() };
        super::proc(&mut x.items, x.mult);
    }
}

In a big project this may result in some of the rust functions having lots of arguments. Surely you can find some smaller groupings of arguments for related groups of functions to help tame this load; higher-level functions especially may want some big structs. When a function takes multiple arguments of the same type, you may also want to add newtype wrappers to prevent transposing the order of two arguments. With those techniques, the rust code may be tedious in places, but not unmaintainable, and the flow of data will be clearer.


If that's still untenable, then the only way to share anything between multiple users is with & references. I.e. everything should take &Ctx, not &mut Ctx. You can wrap small, Copy types in Cell or replace them with atomic types, and put bigger containers in RefCell. If worst comes to worst, some values may need to be stored inside UnsafeCell.

4 Likes

I think the solution is just that! Make one big fat object that owns the data and just keep a ref to it. Thanks!!

As ctx.mult is not modified mut modifier is not needed and I am surprised that no warning is issued due to #[warn(unused_mut)] on by default

Is seems like is not that kind of mut the one controlled by the warn directive, but this one

warning: variable does not need to be mutable
  --> src/main.rs:13:9
   |
13 |     let mut mult = &mut ctx.mult;
   |         ----^^^^
   |         |
   |         help: remove this `mut`
   |
   = note: `#[warn(unused_mut)]` on by default
1 Like