Idiomatic way to implement a portable stateful library

I have a 2d particle engine I created in C# that I've gotten good feedback on. I'd like to take some of the fundamental ideas of that library and make it more portable, meaning instead of it being a C# library it's a library that exposes a C API to return vertex buffers, index buffers, and possibly shaders that can be hooked into any 3d engine regardless of language.

This is inspired by Dear Imgui, which is a C++ code base that exposes a C API (I think?). It stores state in globals which means you don't have to constantly pass pointers in and out of each API call.

I've done Rust before but only from an application perspective, not from a portability perspective. My library will have to handle a lot of allocation and pooling management, and I need to keep state consistent between calls.

In my mind (and maybe I'm wrong) this poses quite a few issues for implementation in Rust:

If I were implementing this in Zig/C/C++ I'd keep state in globals, so all the logic is fully encapsulated by the C API. While this would not be thread safe I think it's a fair trade off (due to how it's being used) to outright state that consumers should only use this API from a single thread at a time. I'm not really sure how much analogue there is to do this in Rust though. Afaict globals in Rust can't normally utilize heap, but I can get around that with lazy_static. However, this does require me to wrap the state in a mutex. I'm unsure of the performance implications of that if games are calling the functionality multiple times per frame.

I can make the C API boundary pure, where it returns the state as a pointer, and every operation you perform against the library requires you passing the state pointer back in. This of course requires trust that the correct pointer was passed in and do an unsafe cast every API call.

I'm not finding a lot of good examples or blogs about using Rust in this manner and I'm wondering if I'm missing something critical in this design. I'm trying not to relearn C++ just for this project, so any advice on how to idiomatically do this in Rust would be a big help, or if this isn't a good fit in general.

I haven't done much of this either, but I'd probably have a struct that holds your "global" state. That'll make Rust-based unit tests easier, if nothing else. You can either hand a pointer to that struct to C or store it in a thread_local! variable somewhere for the C API.