How to put a `syn::Expr` in a static field?

Maybe a macro crate actually doesn't need that little performance boost, but I wonder how to realize it. syn::Expr doesn't implement Send and Sync, so lazy_static doesn't work here.

I've tried local_thread! but it doesn't give static reference, and if I'm right, a macro expand program only runs on a single thread, so this is possible to be realized.

Thanks.

use syn::{Expr, parse_quote};

// What I'm intend to do
static USE_DEFAULT: Expr = parse_quote!(::std::default::Default::default());

// Notice here we need `Expr` to have a static lifetime
// to live longer than any other lifetime
fn get_default_expr<'a>(specified: Option<&'a Expr>) -> &'a Expr {
    specified.unwrap_or(&USE_DEFAULT)
}

If you have a non-thread-safe type, then fundamentally, you can't hand out 'static references to an instance of it, because then any thread could access it. This is exactly why thread_local!() doesn't give out long-lived references, only a temporary reference in a callback, and also why lazy_static! (and its recommended alternative, once_cell::sync::Lazy) requires the thread safety bounds.

So what you want is not directly possible.

1 Like

I agree with most of your ideas, but there still have two points.

It's not. On the contrary, those that are thread-safe may cause problems. Because !Send will prevent an instance from being passed to other threads, but those that implement Send have no such guarantee.

The other one is

If the whole program is guaranteed to run on a single thread, then this become possible.

You could unsafely add the Sync marker to a custom wrapper type and make the code compile. But if you were to forget that your code is only ever supposed to run on a single thread and share your type across threads, it will blow up on you. Also, since you can't use parse_quote in a const context, you have to wrap your type in a OnceLock, requiring us to add the Send marker to the custom wrapper as well:

use std::sync::OnceLock;
use syn::{parse_quote, Expr};

struct MyExpr(Expr);

// What I'm intend to do
static USE_DEFAULT: OnceLock<MyExpr> = OnceLock::new();

unsafe impl Send for MyExpr {}
unsafe impl Sync for MyExpr {}

// Notice here we need `Expr` to have a static lifetime
// to live longer than any other lifetime
fn get_default_expr<'a>(specified: Option<&'a Expr>) -> &'a Expr {
    specified.unwrap_or(
        &USE_DEFAULT
            .get_or_init(|| MyExpr(parse_quote!(::std::default::Default::default())))
            .0,
    )
}

fn main() {
    get_default_expr(None);
}

Playground.

I got inspired by @Michael-F-Bryan's comment on GitHub.

Yes, but implement Send and Sync unsafely without additional guarantee is unsound. I wonder is there a way to ensure that this program runs on single thread only.

You could Box::leak and store the leaked reference in a thread local. Not ideal since this will leak memory, but if get_default_expr is only ever called from a single thread then it shouldn't matter.

use syn::{parse_quote, Expr};

fn get_default_expr<'a>(specified: Option<&'a Expr>) -> &'a Expr {
    thread_local!(
        static USE_DEFAULT: &'static Expr =
            Box::leak(Box::new(parse_quote!(::std::default::Default::default())))
    );

    specified.unwrap_or(USE_DEFAULT.with(|use_default| *use_default))
}
1 Like

Why would it be problematic to call the function from multiple threads? Wouldn't it only mean more leaked memory or is there anything else that could go wrong?

Thanks a lot. That is sound in the macro expand program, which exits rapidly. But as you mentioned, it'll be better if it doesn't leak the memory.

The problem exactly is it will cause more leaked memory.

But that's exactly because thread-safe types can be used across threads – that's what it means to be thread-safe.

Absolutely. But that's why LocalKey has the function with, such a complex way rather than giving a static reference, just to ensure the destructor could runs correctly.
If not, if a static reference of a thread-safe instance goes to other thread, after this thread exited, the memory that reference points to invalidated.

No, that's incorrect. It's not just for the destructor. As already mentioned, giving out a static reference would be unsound. A static reference could be stored in another, non-thread-local static item, which would become dangling when the thread exits.

The point of with is not to run the destructor, that could even be leaked. The point is that when the thread exits the the memory in which thread locals live gets invalidated, thus it's incorrect to yield 'static reference to them.

The value in the thread local doesn't necessarily need to be Sync for this to be exploitable, you just need to be able to get a reference of a Sync value out of it (e.g. a field of it). You would at least need to ensure that no part of it can be shared with another thread, which is different from !Sync which just implies that something can't be shared in its totality with another thread.

2 Likes

Not naively and not in the general case, and I've seen it expressed that making something that attempts to do so in any official way would be counter to Rust's goals.

Unfortunately I have no direct citation, but there's a bunch of discussion in this rather long thread.

Thanks @H2CO3 and @SkiFire13 for reminded me what 'static means. If one reference is 'static, then that reference must always be valid, so does the instance it points to, until the whole process exits rather than any single thread exits (of course the main thread exits means the process exits). But on the other hand, that doesn't give any guarantee that the instance being pointed to by a static reference is sured to be dropped.

Uh, I'm sorry but probably my English skill is not very well so I couldn't understand this sentence completely. Do u mean attempting to find a way to make sure a program must run on single thread is counter to Rust's design goal? If so, which way you prefer, leak the memory as @SkiFire13 suggested, or clone the instance if possible, or some other method else?

One of the goals of Rust is "fearless concurrency". If there was a flag you could pass or similar to deny multithreadedness, it would be a breaking change for a library to spawn a thread when it didn't before, say.

The best alternatives depend on the situation. If your use case

actually doesn't need that little performance boost

then... just don't?

Static items don't drop and if a &static to some value exists, it's UB to Drop::drop it, as that requires exclusivity (&mut).

2 Likes

I got it. Thanks for your answer!

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.