How to pass a shared pointer (Arc) to a synced closure to guarantee its lifetime would outlive a closure?

Hi guys :slight_smile:

I'm trying to use jsonrpc library from parity tech. I'm basing my work on this example. Here's the code with the problem:

use jsonrpc_http_server::jsonrpc_core::*;
use jsonrpc_http_server::{AccessControlAllowOrigin, DomainsValidation, RestApi, ServerBuilder};

struct DataReader<'a> {
    datadir: &'a std::path::Path,
}

impl<'a> DataReader<'a> {
    fn new(data_dir: &'a std::path::Path) -> Self {
        DataReader {
            datadir: data_dir,
        }
    }
}

fn initiate(datadir: &std::path::Path) {
    let mut io = IoHandler::default();

    let datadir_rc = std::sync::Arc::new(datadir.to_owned());

    io.add_method("say_hello", |_params: Params|   // error here
    {
        let x = DataReader::new(datadir_rc.as_path());  // error here 
        Ok(Value::String("hello".to_string()))
    });

    let server = ServerBuilder::new(io)
            .threads(3)
            .rest_api(RestApi::Unsecure)
            .cors(DomainsValidation::AllowOnly(vec![AccessControlAllowOrigin::Any]))
            .start_http(&"127.0.0.1:3030".parse().unwrap())
            .expect("Unable to start RPC server");
}

fn main() {
    let p = std::path::PathBuf::new(); // assume we get this from command-line arguments
    initiate(p.as_path());
}

Unfortunately, I can't put this on playground, because that library is not on playground. For this test, however, this is my Cargo.toml.

[package]
name = "FnTest"
version = "0.1.0"
authors = [""]
edition = "2018"

[dependencies]
jsonrpc-http-server = { git = "https://github.com/paritytech/jsonrpc" }

Now the problem is that the datadir must be posted in every instance of the closure in the async threads (it's a multithreaded program, so multiple copies have to go around. For that (and since I'm originally a C++ programmer), I'm trying to use the C++ way. I tried to pass that path as a shared pointer, so Arc in rust. However, the problem is that rust keeps thinking that the closure may outlive the variable. It's not true though!

Now in C++, you'd guarantee that a shared pointer will outlive the closure by passing the shared pointer as a captured variable, like this (imitating add_method from above):

auto datadir = std::make_shared<const std::string>("/home/user/mypath");
io.add_method("get_data", [datadir](const auto& params) {
    // now datadir is guaranteed to live in all threads
});

How do we do the same in rust?

If what I'm doing here is wrong, what's the right way?

Thanks in advance!

EDIT: I wanna mention that the closure in that method implements the trait Fn.

Hi, by default closures will try to borrow what you give them. To fix your issue you just have to listen to the compiler:

help: to force the closure to take ownership of `datadir_rc` (and any other referenced variables), use the `move` keyword
   |
23 |     io.add_method("say_hello", move |_params: Params|   // error here
1 Like

Moving somehow doesn't make sense to me. I'm not "moving" the shared pointer. I'm only copying it. Isn't that the point of a reference-counted pointer? Can you please explain how moving makes sense here?

When you use move, you're taking the variables from the outer closure's stack frame and pushing them into the inner closure's stack frame (only the variables you use, however). When you move the shared pointer - which implicitly points to data on the heap - you are just doing a pointer copy across the closure boundary. This is my current understanding of what's happening, at least

1 Like

I don't know what add_method will do but it's bound asks for 'static. A type is 'static as long as it doesn't contain any reference or any reference it contains has the lifetime 'static.

Going from here you don't even need an Arc, you just need to move datadir_rc inside the closure. This way the closure won't borrow anything and have a 'static type.

You can learn more about closures in this article and this one.

Here is the working example, if you get a new error in your actual code, we'll iterate on it =)

2 Likes

Thanks for the explanation. I'll try to think of it more and ingrain it in my brain. This is kind of unusual way to think of it.

Btw, the closure that method uses implements Fn. I'm not sure it's possible to move there. But I'll try it tomorrow. Today I can't anymore.

2 Likes

Thanks for the advice. I remember trying to move things inside the closure without success. Always something complained. I'll try again tomorrow (I gotta go now).

Btw, it'd be great if you could provide a working example for this. If you have the time for it :slight_smile:

Leudz sch00led me. Thanks mate

You're a physicist. Okay, so this is actually how I go about thinking of it. There is a gravity model with stack frames and closures. Everything that gets pushed onto the stack eventually gets popped out of the stack. You can think of the heap (when using Box) as a hover craft... so long as the item remains boxed, it wont fall to the ground as the stack frames lose all of its elements. The reason why thinking of it like this is useful is because it pushes across the idea that things should be as short lived as possible (for performance reasons), but if they need to live across closure boundaries, then you need the variable to get pushed onto the heap (i.e., the hovercraft).

This might be true for other languages but not Rust. Closures are just struct created by the compiler. They can be on the stack or the heap like any other struct, but they won't get push on the heap if you don't ask for it.
Here's an example:

#[derive(Debug)]
struct Int(u32);

fn main() {
    let int = Int(1);
    let int_gen = move || int;
    dbg!(int_gen());
}

int is created in main's stack as well as int_gen. int_gen contains a single field containing a Int. Since we use move, Int(1) will be moved inside int_gen. When the closure is called, the Int is extracted from the closure and get displayed. Nothing ever gets to the heap, it's all stack.
This is without optimization of course, LLVM will probably never make the closure nor any variable in this example.
I hope this is somewhat clear, the two articles I linked earlier explain this way better than me.

2 Likes

Here's almost the same question with a thread of various explanations.

Since you come from C++, let's use it for the explanation:

A |...| { ... } closure (i.e., non move annotated) lets Rust pick the binding mode, depending on the body of the closure (it depends in how the captured variable is used).

If you look at your closure closely, you'll realise that if it only had captured a [&datadir] rather than [datadir], then, for the body alone (i.e., ignoring the 'static lifetime requirement), it would have been fine, meaning that capturing [&datadir] suffices. And if it suffices, Rust chooses that. Hence @leudz's statement "by default [in most cases] closures will try to borrow what you give them" rather thank take it directly.

To make the closure's capture be [datadir], i.e., to make it take datadir, i.e., to make datadir be moved into the closure (that's how we call it in Rust: move to mean by-value rather than by-ref), we can override Rust's chosen binding mode with the "take every variable directly by value" with the move prefix qualifier on the closure.

If something has a destructor, and you need to have an owned version of it (to ensure it does not dangle in the meantime), then you either move it (std::move in C++ parlance) or you .clone() it (copy constructor in C++ parlance).

So, when you have:

auto rc_obj = std::make_shared<MyObject>();
auto func =
    [rc_obj] // sugar for [rc_obj = rc_obj] /* i.e. copy construction */
    {
        // do stuff with rc_obj
    }
;

then you are implicitly using the copy constructor, meaning, for instance, that the ref-count has been incremented. In Rust this is explicit, and written as:

let rc_obj = Arc::new(MyObject::new());
let func = {
    let rc_obj = rc_obj.clone();
    move || {
        // do stuff with rc_obj
        let _ = &rc_obj;
    }
};

If you do not intend to use rc_obj outside that closure, then incrementing a counter there to later decrement the counter when the original (and unused) rc_obj goes out of scope is slightly suboptimal.

In that case you do:

auto rc_obj = std::make_shared<MyObject>();
auto func =
    [rc_obj = std::move(rc_obj)] // move constructor!
    {
        // do stuff with rc_obj
    }
;

which removes the useless increment-then-decrement by, in a way, having simplified it at compile-time thanks to the semantic assertion of std::move. And this in Rust becomes:

let rc_obj = Arc::new(MyObject::new());
let func = move || {
    // do stuff with rc_obj
    let _ = &rc_obj;
};

You will notice how Rust makes it far more idiomatic / terse to move, contrary to C++'s implicit usage of the copy constructor (the perks of being a language born after move semantics rather than before
:grin:).

11 Likes

Thanks for the explanation. Very valuable. I think my confusion comes from the fact that in C++ we explicitly copy/move to the closure's capture list, while in Rust it's implicit. This kind of implicit thing is scary to me, since I "grew up with C++" to be paranoid about anything that "I don't see". Because in C++, the way I think of things is "if I don't know how it's working, it might be unsafe". The thought that the counter will increment and decrement is inefficient, 100% true, but it makes me feel better that the shared pointer will never die due to some intermediate magic :smiley: (especially that this is async stuff... the pure recipe of dangling references)

Rust is quite the opposite. Everything unsafe just doesn't work. It automatically manages the movement and I don't have to worry about it. I need some time to adjust to that.

Thanks again :slight_smile:

3 Likes

If you want to know exactly how closures are desugared, read my blog on the subject!

3 Likes

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.