[Beginner] Using a OnceLock<Arc<str>>

Dear Community :grinning:. I am a new Rust user, and I am just working my way into the Rust world. So far, I have 20 years of C# experience. As my first Rust project, I'm working on a simple desktop tool, which I'm developing with Tauri and Leptos (WASM, https://www.leptos.dev/).

My first Rust steps were very enjoyable, so I fell in love with Rust right away :heart_eyes:. However, I'm now stuck at a point where I can't get anywhere without you guys.

It started when I saw the video "Use Arc Instead of Vec" by Logan Smith (Use Arc Instead of Vec - YouTube). In the video, Logan explains that str works just as well instead of Vec. Well, I just had a string in my Rust program and wanted to try his hint just for practice.

Basically, it worked well and I came to the following result. This is a GUI component in Leptos - so in the frontend with WASM. I implemented Logan's idea here for the file path path_arc (which comes from the backend) and for its default value no file selected:

use std::sync::{Arc, OnceLock};
use leptos::*;
use wasm_bindgen::prelude::*;
use crate::components::{button::*};

#[wasm_bindgen]
extern "C" {
    #[wasm_bindgen(js_namespace = ["window", "__TAURI__", "tauri"])]
    async fn invoke(cmd: &str, args: JsValue) -> JsValue;
}

#[component]
pub fn Welcome(cx: Scope) -> impl IntoView {

    let default_text = "no file selected".to_string();
    let default_value: Arc<str> = Arc::from(default_text);
    let (chosen_file, set_chosen_file) = create_signal(cx, default_value);

    let import = move || {
        spawn_local(async move {
            let import_file_path_temp = invoke("import_file", JsValue::UNDEFINED).await;
            if let Some(import_file_path) = import_file_path_temp.as_string() {
                let path_arc: Arc<str> = Arc::from(import_file_path);
                set_chosen_file.set(path_arc);
            }
        });
    };

    view! { cx,
        <div class="d-flex flex-column justify-content-center align-items-center" style="height: 80vh;">
            <img src="public/icons/splashscreen.png" alt="Main Icon" width="128" height="128"/>
            <h1>"Welcome"</h1>

            <div class="mt-5">
                <ImageButton icon_name="import" text="Import" on_click=import />
                <ImageButton icon_name="export" text="Export" on_click=|| {} />
            </div>

            <div>
                "Debug ... chosen file: " {move || chosen_file.get().to_string()}
            </div>
        </div>
    }
}

Coming, as I said, from the C# world, I thought: there I would define the default value once e.g. in the constructor for a static readonly field or ideally as const field. That's how I learned about OnceLock and OnceCell. I have tried to implement this:

use std::sync::{Arc, OnceLock};
use leptos::*;
use wasm_bindgen::prelude::*;
use crate::components::{button::*};

fn default_value<'a>() -> &'a Arc<str> {
    static DEFAULT_VALUE: OnceLock<Arc<str>> = OnceLock::new();
    DEFAULT_VALUE.get_or_init(|| Arc::from("no file selected"))
}

#[wasm_bindgen]
extern "C" {
    #[wasm_bindgen(js_namespace = ["window", "__TAURI__", "tauri"])]
    async fn invoke(cmd: &str, args: JsValue) -> JsValue;
}

#[component]
pub fn Welcome(cx: Scope) -> impl IntoView {

    let (chosen_file, set_chosen_file) = create_signal(cx, default_value());

    let import = move || {
        spawn_local(async move {
            let import_file_path_temp = invoke("import_file", JsValue::UNDEFINED).await;
            if let Some(import_file_path) = import_file_path_temp.as_string() {
                let path_arc: Arc<str> = Arc::from(import_file_path);
                set_chosen_file.set(&path_arc);
            }
        });
    };

    view! { cx,
        <div class="d-flex flex-column justify-content-center align-items-center" style="height: 80vh;">
            <img src="public/icons/splashscreen.png" alt="Main Icon" width="128" height="128"/>
            <h1>"Welcome"</h1>

            <div class="mt-5">
                <ImageButton icon_name="import" text="Import" on_click=import />
                <ImageButton icon_name="export" text="Export" on_click=|| {} />
            </div>

            <div>
                "Debug ... chosen file: " {move || chosen_file.get().to_string()}
            </div>
        </div>
    }
}

However, this does not work: Instead of a Arc<str>, I now have a &Arc<str>, which seems logical to me at first. After all, I don't want a clone to be created but the default value to be reused. It also works so far, except for the assignment of the path that comes from the backend set_chosen_file.set(&path_arc);. I get the error 'path_arc' does not live long enough [E0597] borrowed value does not live long enough.

I read in the Rust book, of course. After reading chapter 4 "Understanding Ownership", I thought I can somehow move the ownership, away from the current local scope into the signal; so that my variable path_arc cannot be used in the scope afterward. But this is precisely where I am currently stuck. Somehow I haven't quite understood the concept yet...

Can anyone help me with this :slightly_smiling_face:? The whole thing is just for practice, because, of course, I could have all the code running either without Arc<str> or at least without OnceLock.

Best
Thorsten

If you use clone to turn the &Arc<str> into an owned Arc<str>, that clone operation will be the cheap O(1) operation that merely increases the reference count. The underlying string data is only ever created (allocation + copying the data) once thanks to OnceLock. So just doing

fn default_value() -> Arc<str> {
    static DEFAULT_VALUE: OnceLock<Arc<str>> = OnceLock::new();
    Arc::clone(DEFAULT_VALUE.get_or_init(|| Arc::from("no file selected")))
//  ^^^^^^^^^^                           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
//    |                                    |
//   This function call is super           +--- this part is more expensive,
//   cheap (increments reference count)         but only happens once 
}

is exactly what you want.

3 Likes

Wow, thanks @steffahn for the prompt answer :grinning:. That just works.

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.