Beginner: edit variable outside a closure

Hello,
I am a very beginner with rust and I want to use it to achieve a simple temperature reading on my BLE thermometer.
I use the rumble crate for that.

Here is my full code, derived from rumble example:

extern crate rumble;

use std::str;
use std::process;
use std::thread;
use std::time::Duration;
use rumble::bluez::manager::Manager;
use rumble::api::{UUID, ValueNotification, Central, Peripheral};

const TARGET_DEVICE_NAME: &str = "MJ_HT_V1";
const TARGET_CHARACTERISTIC_UUID: UUID =
UUID::B128([0x6d, 0x66, 0x70, 0x44, 0x73, 0x66, 0x62, 0x75, 0x66, 0x45, 0x76, 0x64, 0x55, 0xaa, 0x6c, 0x22]);

fn main() {
    let manager = Manager::new().unwrap();

    // get the first adapter
    let adapters = manager.adapters().unwrap();
    let mut adapter = adapters.into_iter().nth(0).unwrap();

    // reset the adapter -- clears out any errant state
    adapter = manager.down(&adapter).unwrap();
    adapter = manager.up(&adapter).unwrap();

    // connect to adapter
    let central = adapter.connect().unwrap();

    // start scanning for devices
    central.start_scan().unwrap();

    // instead of waiting, you can use central.on_event to be notified of new devices
    thread::sleep(Duration::from_secs(1));

    let device = central.peripherals().into_iter()
        .find(|p| p.properties().local_name.iter()
              .any(|name| name.contains(TARGET_DEVICE_NAME)));

    let temperature_sensor;
    match device {
        Some(device_found) => {
            temperature_sensor = device_found;
            println!("Device found");
        },
        None => {
            println!("Device not found");
            process::exit(1);
        }
    };

    // connect to device
    temperature_sensor.connect().unwrap();

    // discover characteristics
    temperature_sensor.discover_characteristics().unwrap();

    // get characs
    let characs = temperature_sensor.characteristics();

    // get temperature charac
    let temperature_char = characs.iter().find(|c| c.uuid == TARGET_CHARACTERISTIC_UUID).unwrap();

    temperature_sensor.on_notification(Box::new(|n: ValueNotification| {
       println!(String::from_utf8(n.value).unwrap());
    }));

    temperature_sensor.subscribe(temperature_char);

    thread::sleep(Duration::from_secs(10));

    temperature_sensor.disconnect();
}

I want to exit the program as soon as I have been notified of a new value from the closure I passed to the on_notification event of the temperature_sensor. However, I just don't know how to get the information outside of the closure because this only a Fn closure and mutate outside variables is hence not allowed.
What is the right way to do that ?

You could use a std::cell::Cell, this will allow you to mutate stuff inside a Fn closure.

Thanks for your response. I edited the code:

let charac_read = Cell::new(false);
temperature_sensor.on_notification(Box::new(|n: ValueNotification| {
   println!("{}", String::from_utf8(n.value).unwrap());
   charac_read.set(true);
}));

temperature_sensor.subscribe(temperature_char);

while (charac_read.get() == false)
{
}

But this won't compile either, as std::Cell cannot be shared between threads safely.
What can I do then ?

You can use an Arc<Mutex> or an Arc<RwLock>

Since you are using a bool, you can also use AtomicBool

2 Likes

Thanks to both of you but I am still lost a bit.
AtomicBool seems to be what I was looking for but I have the same problem than at the beginning:

let charac_read = AtomicBool::new(false);
temperature_sensor.on_notification(Box::new(|n: ValueNotification| {
   println!("{}", String::from_utf8(n.value).unwrap());
   charac_read.store(true, Ordering::Relaxed);
}));

temperature_sensor.subscribe(temperature_char);

while charac_read.load(Ordering::Relaxed) == false
{
}

This won't compile because borrowed value does not live long enough.

I tried that as well :

let charac_read = Cell::new(AtomicBool::new(false));

temperature_sensor.on_notification(Box::new(|n: ValueNotification| {
   println!("{}", String::from_utf8(n.value).unwrap());
   charac_read.get().store(true, Ordering::Relaxed);
}));

temperature_sensor.subscribe(temperature_char);

while charac_read.get().load(Ordering::Relaxed) == false
{
}

But It won't compile since no method named get found for type std::cell::Cell<std::sync::atomic::AtomicBool> in the current scope
= note: the method get exists but the following trait bounds were not satisfied:
std::sync::atomic::AtomicBool : std::marker::Copy

You don't need to wrap the AtomicBool in a Cell, just pass a &AtomicBool

Hum sorry but where am I supposed to pass a &AtomicBool ?
I never pass the boolean anywhere (at least in a visible way)

Sorry I only read the Cell part, I was thinking about just making the atomic like you did but since you box it maybe you can't use a reference, what about Arc<AtomicBool>?

I introduces Arc:

let charac_read = Arc::new(AtomicBool::new(false));

temperature_sensor.on_notification(Box::new(|n: ValueNotification| {
   println!("{}", String::from_utf8(n.value).unwrap());
   charac_read.store(true, Ordering::Relaxed);
}));

temperature_sensor.subscribe(temperature_char);

while charac_read.load(Ordering::Relaxed) == false
{
}

But it does not compile: borrowed value does not live long enough.

Does this work?

let charac_read = Arc::new(AtomicBool::new(false));
let clone = charac_read.clone();

temperature_sensor.on_notification(Box::new(move |n: ValueNotification| {
   println!("{}", String::from_utf8(n.value).unwrap());
   clone.store(true, Ordering::Relaxed);
}));

temperature_sensor.subscribe(temperature_char);

while charac_read.load(Ordering::Relaxed) == false
{
}

Nope, same error

Just to be sure you put the "move" in Box::new(move |
If it is the case, I really don't know and I'm curious about the answer.

Nope i hadn't noticed it, so sorry. It compiles right now. Thanks you very much.
I will need a lot of time to understand what you made me do.
I have mostly developed in C/C++/C# and for now, I miss a basic theoric understanding of all borrowing and ownership concepts.

No one comes to Rust with an understanding of borrowing and it's implications. That's why Rust has a reputation for being hard to learn.

1 Like

Your starting point:

you have a local variable charac_read

  • the fact it is local is important since, an alternative solution to using Arc would have been declaring it global / static with:

    static charac_read: AtomicBool = AtomicBool::new(false);
    
    temperature_sensor.on_notification(Box::new(|n: ValueNotification| {
       println!("{}", String::from_utf8(n.value).unwrap());
       charac_read.store(true, Ordering::Relaxed);
    }));
    
    temperature_sensor.subscribe(temperature_char);
    
    while charac_read.load(Ordering::Relaxed) == false
    {
    }
    

The problem of borrowing a local variable

Why does the above code or the one with Arc work, or in other words, why did the code without both static and arc not work?

Because what the closure was capturing ("implicitly using") was a shared reference (e.g., &'a AtomicBool) to charac_read. However, once you spawn a thread (let's call it the "spawnee"), it may outlive the thread that spawned it!! (let's call it the spawner). Since the spawnee can outlive the spawner, it is thus able to witness the spawner returning from its function (in your example, main). So, if the spawnee was allowed to borrow locals from the spawner, the spawned closure could be referring to freed data!

That's why (the closure describing the behavior of) the spawnee cannot borrow locals (if you try to do it, you will get errors saying that the borrowed value does not live long enough, since it must not have a local lifetime 'a but the infinite lifetime called 'static).

  • in ::rumble's case, on_notification takes a Box<dyn Fn(ValueNotification) + Send + 'static>, so you can "see" the 'static lifetime requirement at the API level (actually the lifetime annotation is ellided, but when that happens with a trait object type definition, 'static is the lifetime "picked by Rust").

How to thus avoid borrowing a local variable

  • you may sometimes be able to use a static variable (when its initial value can be computed at compile time, such as with the literal false);

  • else you must wrap your variable in a Arc (Atomically Reference-Counted smart pointer, the equivalent of C++'s shared_ptr) to get multiple ownership (only when all the owners have been dropped does the value get deallocated, thus preventing the aforementioned use-after-free problem):

    • to create another owner, you just .clone() the smart pointer,

    • to give/send/move the newly created owner to the other thread, the name of the new owner must appear in the body of the closure (as usual) and the closure must be declared with the move keyword before its arguments (the part between pipes),

  • another overlooked option is to "promote a local to static" by leaking it (requires that the data be stored in the heap, e.g., boxed):

    1. Box your local

      let charac_read: Box<AtomicBool> = Box::new(AtomicBool::new(false));
      
    2. leak it:

      let charac_read: &'static AtomicBool = Box::leak(charac_read);
      // now you can use `charac_read` in the spawned thread;
      
3 Likes

It makes sense, indeed. Thanks for the clarification.

There is a point that I don't understand: why is the callback boxed ?

Is there any runtime penalty due to reference counting ?

Nice trick

Many thanks for your responses and your explanations.
Very appreciated and useful.

1 Like

That is a choice of the API, I guess due to FFI constraints that require a specific type; although it is possible to "do FFI and generics", it is far more tedious / cumbersome for the library author, and more importantly, far more code is generated (one concrete function for each monomorphisation over the closure type), which, in the case of embedded, may be definitely noticeable.

The other path, taken here, is to abstract over all the closure types with the dyn Fn(...) -> ... trait object. The only way to technically unify stuff of potentially different size into a single type is through a layer of indirection (and a vtable to dynamically dispatch method calls), hence you will never see the type dyn Fn directly used but behind a (fat) pointer:

  • Box<dyn Fn(...)> for an owning pointer,

  • &dyn Fn() for a borrowed shared pointer,

  • &mut dyn FnMut() for a borrowed unique pointer

Yep, the initial heap allocation, and then the actual reference counting :slight_smile:: an atomic counter increment for every clone, and an atomic counter decrement for every drop (and the final heap deallocation). In practice it can be quite negligeable, specially compared to heap allocation or thread spawning.

Thus in your example I'd definitely use the static variable declaration route.

2 Likes

Ok ! Thank you again for your very clear explanations.
It helps a lot.

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