Sdl2 and rc type puzzle

I'm in the processing of rewriting an existing C application that uses SDL2 in rust.

As part of that process, I'm incrementally converting the existing program and have been doing that by building a rust static library that the C program links against with all of the functions exported using the C ABI which has been working great.

I've reached the point now where I need access to the SDL context:

However, I've hit an unexpected blocker. The main program loop isn't ready to be converted to rust yet, and the sdl2 rust bindings return a special Rc<>-based object as SDL requires that SDL functions are only called from the main thread.

Because the rust library must be the one to create the Sdl context so that I can call various methods related to it, I would like it to be a global static in the library so I can continue to incrementally rewrite the rest of the C program after the C program calls a library function to init SDL.

I found lazy_static, which seemed promising, but I haven't figured out the incantation required to use it. What I have so far is something like this:

extern crate sdl2;
#[macro_use]
extern crate lazy_static;

lazy_static! {
    pub static ref sdl_context: &'static sdl2::Sdl = {
        sdl2::init().unwrap()
    };
}

This fails to compile, unsurprisingly, with something like:

error[E0277]: `Rc<SdlDrop>` cannot be shared between threads safely
   --> chroma\src\lib.rs:337:1
    |
337 | / lazy_static! {
338 | |     pub static ref sdl_context: &'static sdl2::Sdl = {
339 | |         sdl2::init().unwrap()
340 | |     };
341 | | }
    | |_^ `Rc<SdlDrop>` cannot be shared between threads safely

I'm well aware that Rc<> things cannot be shared between threads safely. However, I won't be using this from anywhere but the main thread -- this is, in practice, a single-threaded program.

I haven't found any examples and I'm hoping someone else has had to do something like this before or can explain what minimal change I can make to make the compiler happy.

Using thread_local here didn't seem like the right solution here unless that's just the way of making the compiler shut up and it's fine since I'll only ever really be using one thread anyway.

I'm guessing I need to use RefCell instead of lazy_static? Ideas?

1 Like

Why do you think so? It seems you literally want a global variable which is local to the main thread.

1 Like

You can put the context in something like send_wrapper before you store it in the lazy_static, which will enforce the single-thread access even though other threads can see the variable.

For me, thread_local seems designed to have one instance per thread, which is not the desired result here. Instead, the goal is to have only one instance, regardless of the number of threads, but that can only be accessed from one.

This is a common need when interacting with external libraries, but it's not obvious how to achieve this from the LocalKey documentation.

1 Like

Well I prefer scoped_tls crate over the std::thread_local for many reasons including the one you've mentioned. But it still is backed by the thread-local storage so I asked why OP doesn't liked it from the start.

1 Like

For exactly the reason the other poster stated. I don’t want an instance per thread, I want a global instance. Yes, logically there’s only one thread so it shouldn’t matter, but I don’t generally reach for TLS unless I need it.

So what’s the “right” way to have a global instance here? Wrap this Rc in a mutex? Magic incantation using RefCell?

It just feels like there should be a more formal way than just wrapping it with thread_local. Particularly if I want to make it clear that there is only a single instance ever and it can’t be used from any thread but the main thread.

Basically, how do I enforce at compile time that there is only a single instance and that instance can only be used from the main thread? Can I do that?

Well the thing is that the "main thread" is not special to the compiler. fn main() is just a normal function, you can even call it from another function within another thread.

With the scoped_tls crate I mentioned "instance" is stored in the function local variable and only its reference is passed through the call stack via thread-local storage. Nothing magically initialized per every threads - you need to manually initialize it with the explicit function call.

Usually, it’s exactly this sort of accidentally calling something from the “wrong” thread that should be protected against— If a library insists that something should be a singleton and only accessed from one thread, thread-local memory seems like the wrong choice. That’s why I suggested storing a send_wrapper in a global— You’re guaranteed to have only one, and accessing from multiple threads will raise an error.

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.