Introduction
You'll probably want to turn off whitespace changes to weed out indentation changes in the code.
This PR is a collection of miscellaneous API improvements I tinkered with over the weekend. The goals for these changes, in rough order of importance, were:
- Expose the full functionality of the libretro API.
- Future-proof the API against new environment commands.
- Enforce more of libretro's rules using types.
- Make the crate's abstractions as close to zero-cost as possible.
- Minor project housekeeping (e.g. update the gitignore.)
I wanted to get these breaking changes out of the way now before moving on to adding more environment commands and features.
Major Changes
Environment overhaul
Scoped environments
Addresses #2 and lays the groundwork for #16. The new implementation exposes an environment trait for every retro_*
function, so that we can limit which non-raw environment commands are available. The *_raw
methods are always available, since they're the user's escape hatch for functionality the crate doesn't implement yet.
I initially tried to implement this by adding a PhantomData
parameter to the environment, but this required casting in every RetroInstance
callback as well as polluting the API with a lot of marker structs. I found the trait-based implementation much more straightfoward, since the C callback can implement all of the environment traits simultaneously, and it allows users to unit test their RetroCore
since they no longer have a hard-coded dependency on the C function pointer.
Reworked raw methods
The base environment trait defines its raw_*
methods in terms of Rust types, not pointers, and there's a new method fn mut_ref_raw<T>(&mut self, key: u32, data: &mut T)
for commands that mutate structs.
All raw methods are unsafe
All raw methods should be unsafe
since they allow the user to violate libretro's requirements and we can't account for future additions to the environment callback.
RetroCore API improvements
Exposed every retro_*
function in RetroCore
Because libretro can add new environment commands at any time, and those commands may require being called during specific retro_*
callbacks, the only way to expose libretro's full functionality and future-proof the API is to surface all of the retro_*
to the user.
Other benefits of this change are less glue code in RetroInstance
and a much tighter correspondence between the Rust API and C API, which should help users cross-reference the documentation.
Removed redundant method parameters.
Some functions in the C API pass an array pointer and the array's size as separate parameters. In RetroCore
, the array pointers were translated into Rust slices, but the redundant size parameter wasn't removed. Since slices carry their length, I've removed these size parameters.
Refine c_uint parameters.
libretro's ABI uses unsigned int
for all integers, even if its parameters would fit into a byte or short. To the best of my ability, I've replaced most c_uint/u32
parameters with the narrowest fixed-size type that will accept all of its values.
Additionally, two of these parameters pass user-defined values that are established in environment commands. These have been replaced with two associated types:
pub trait RetroCore: Sized {
type SpecialGameType: TryFrom<u8>;
type SubsystemMemoryType: TryFrom<u8>;
fn load_game_special(&mut self, env: &mut impl LoadGameSpecialEnvironment, game_type: Self::SpecialGameType, info: RetroGame) -> bool;
fn get_memory_data(&mut self, env: &mut impl GetMemoryDataEnvironment, id: RetroMemoryType<Self::SubsystemMemoryType>) -> Option<&mut [u8]>;
// etc.
This will allow users to use their own enums, provided the enum is convertible into a u8
. Cores that have no need for this functionality can use the provided empty tuple struct NotApplicable
:
impl RetroCore for Emulator {
type SpecialGameType = NotApplicable;
type SubsystemMemoryType = NotApplicable;
// etc.
}
Overhauled string handling
Some C strings returned by libretro are not guaranteed to be UTF-8 encoded. The crate should expose these as &CStr
and let the user decide if they want to assume they're UTF-8 or handle them some other way. Convenience methods have been provided to convert Option<&CStr>
to Option<&str>
in one step.
Additionally, the API uses the CUtf8
crate when exposing UTF-8 C Strings, since this type preserves the fact that the string slice is both UTF-8 and nul-terminated. This allows for trivial conversions to both &CStr
or &str
. &str
can't do this because once a &CStr
is converted to &str
, you've discarded the fact that it was originally nul-terminated.
Internal changes
Zero-cost struct conversions
Rust versions of the C structs generated by bindgen are now implemented as #declare(transparent)
newtype wrappers. The public API still uses idiomatic Rust types, but the conversion to a libretro struct should be a no-op:
/// Rust interface for [retro_system_timing].
#[repr(transparent)]
#[derive(Clone, Debug)]
pub struct RetroSystemTiming(retro_system_timing);
impl RetroSystemTiming {
/// Main constructor.
pub fn new(fps: f64, sample_rate: f64) -> Self { Self(retro_system_timing { fps, sample_rate }) }
pub fn fps(&self) -> f64 { self.0.fps }
pub fn sample_rate(&self) -> f64 { self.0.sample_rate }
pub fn into_inner(self) -> retro_system_timing { self.0 }
}
Modularized the crate
lib.rs
was getting crowded, so wherever I overhauled a type I moved it to a new file. For the sake of making the diff easier to verify, any code that only required minor changes was left in lib.rs
.
Removed libc
To the best of my knowledge, we weren't using it for anything that core::ffi::*
didn't already provide.
Updated bindgen version
I updated bindgen to the latest version. This resulted in 2 or 3 lines in RetroInstance
having some casts adjusted.
core
over std
I've replaced references to std
with core
wherever possible, which should help us be more conscious of places where we're doing allocations and may allow the crate to function in a no_std
context.
Afterword
Sorry for the big PR. I wasn't expecting to submit this much at once, but as I was experimenting one thing just led to another. As usual, let me know where you think a change missed the mark.