Hi there, I've been using sdl3-sys and writing some simple SDL3 wrappers. My current (WIP) code looks like this:

 let sdl_system = SDLSystem::new(&sdl_metadata) .map_err(|err| format!("Failed to create SDL system: {}", err))?; let sdl_window = SDLWindow::new(&sdl_system, &sdl_window_settings) .map_err(|err| format!("Failed to create SDL window: {}", err))?; let sdl_opengl = SDLOpenGL::new(&sdl_window) .map_err(|err| format!("Failed to create OpenGL context: {}", err))?; 

With this for the SDLSystem code;

 // Represents an initialized SDL and subsystems pub struct SDLSystem; impl SDLSystem { fn init_subsystems() -> Result<(), SDLError> { // SAFETY: Fixed input, no sharing let inited = unsafe { SDL_InitSubSystem(SDL_INIT_VIDEO) }; SDLError::result_err(SDLErrorType::Init, inited)?; // SAFETY: Fixed input, no sharing let gl_loaded = unsafe { SDL_GL_LoadLibrary(std::ptr::null()) }; SDLError::result_err(SDLErrorType::Init, gl_loaded) } fn set_metadata(key: &str, value: &str) -> Result<(), SDLError> { let key_cstr = CString::new(key).expect("CString::new failed"); let value_cstr = CString::new(value).expect("CString::new failed"); let key_ptr = key_cstr.as_ptr(); let value_ptr = value_cstr.as_ptr(); // SAFETY: Pointers are valid, SDL copies the values given let success = unsafe { SDL_SetAppMetadataProperty(key_ptr, value_ptr) }; SDLError::result_err(SDLErrorType::Metadata, success) } pub fn new(metadata: &SDLMetadata) -> Result<SDLSystem, SDLError> { Self::set_metadata("SDL.app.metadata.name", &metadata.name)?; Self::set_metadata("SDL.app.metadata.version", &metadata.version)?; Self::set_metadata("SDL.app.metadata.identifier", &metadata.identifier)?; Self::set_metadata("SDL.app.metadata.creator", &metadata.creator)?; Self::set_metadata("SDL.app.metadata.copyright", &metadata.copyright)?; Self::set_metadata("SDL.app.metadata.url", &metadata.url)?; Self::set_metadata("SDL.app.metadata.type", "game")?; // TODO: Add openGL version Self::init_subsystems()?; Ok(SDLSystem {}) } } 

and this for the SDLWindow:

 pub struct SDLWindow<'a> { window: *mut SDL_Window, system_marker: PhantomData<&'a SDLSystem>, } impl<'a> SDLWindow<'a> { fn create_with_properties( _system: &'a SDLSystem, properties: SDLProperties<'a>, ) -> Result<SDLWindow<'a>, SDLError> { // SAFETY: Video subsystem is ready, properties is valid let window = unsafe { SDL_CreateWindowWithProperties(properties.id) }; let success = !window.is_null(); SDLError::result_err(SDLErrorType::Window, success)?; Ok(SDLWindow { window: window, system_marker: PhantomData, }) } pub fn new( system: &'a SDLSystem, settings: &SDLWindowSettings, ) -> Result<SDLWindow<'a>, SDLError> { let mut properties = SDLProperties::new(&system)?; properties.set_string("SDL.window.create.title", &settings.title)?; properties.set_number("SDL.window.create.width", settings.width as i64)?; properties.set_number("SDL.window.create.height", settings.height as i64)?; properties.set_number("SDL.window.create.x", SDL_WINDOWPOS_CENTERED.into())?; properties.set_number("SDL.window.create.y", SDL_WINDOWPOS_CENTERED.into())?; properties.set_bool("SDL.window.create.opengl", true)?; properties.set_bool("SDL.window.create.resizable", true)?; Self::create_with_properties(system, properties) } } 

Things are going well so far (I've omitted some more of the messy details) but I do have a few questions about this.

The first is how I should enforce SDLSystem being a singleton. It's unsound to create another SDLSystem as it will drop before the other and invalidate all my references. I plan to just use a global variable and assert, but something with lifetimes sounds better. I was thinking of having SDLSystem be a mutable global object you borrow on success?

The second is that I think my idea of 'SAFETY' here is wrong. SAFETY is for memory safety, right? I'm writing a lot of invariants like 'Video subsystem is ready' which is more of a runtime safety check that lifetimes help me enforce. If the subsystem wasn't ready it would just generate an error.

The third is whether I should be using 'mut' for SDLSystem and friends. This wouldn't work very well at all because I need multiple references to different objects, which makes exclusivity not possible. So at the very least I'm modelling interior mutability.

But doesn't that kind of defeat some purposes of mutability? Even interior mutability? If it's possible to have SDLSystem change something, that's action at a distance.

I'm sorry for the somewhat silly questions but when there are terms thrown around like 'unsafe', 'unsound', 'memory safety', I find it hard to understand how it applies to code like this where yes I'm not doing anything memory unsafe (SDL might though), but I'm also side stepping what people might expect from an object being immutable.

Another question to throw in is that I want to make it impossible to use my structures across threads. I've seen this struct value in the sdl3 crate source:

marker: PhantomData<*mut ()>, 

My understanding is that pointers automatically disable Send/Sync and PhantomData lets us have that for free at the type level. But why the mut?

Sorry for the barrage of questions.

2 posts - 2 participants

Read full topic