WIP idiomatic wrapping of lower-level FFI

This commit is contained in:
lif 2019-11-10 16:24:29 -08:00
parent 0d0dda356c
commit 5b27d27fdf
7 changed files with 340 additions and 35 deletions

View File

@ -8,6 +8,7 @@ edition = "2018"
failure = "^0.1"
libloading = "^0.5"
num_enum = "^0.4"
structopt = "^0.3"
[build-dependencies]
bindgen = "^0.51" # keep an eye on https://github.com/rust-lang/rust-bindgen/issues/1541

View File

@ -24,6 +24,6 @@ fn main() {
// Write the bindings to the $OUT_DIR/bindings.rs file.
let out_path = PathBuf::from(env::var("OUT_DIR").unwrap());
bindings
.write_to_file(out_path.join("libretro_types.rs"))
.write_to_file(out_path.join("libretro_ffi.rs"))
.expect("Couldn't write bindings!");
}

View File

@ -1,7 +1,10 @@
use std::convert::TryFrom;
use std::os::raw::c_uint;
use std::convert::{TryFrom, TryInto};
use std::os::raw::{c_uint, c_void};
use std::path::PathBuf;
use num_enum::TryFromPrimitive;
use crate::libretro_types::*;
use crate::libretro_ffi::*;
#[derive(TryFromPrimitive)]
#[repr(u32)]
@ -114,25 +117,133 @@ pub enum Input {
Pointer(PointerStat),
}
impl<D> From<(D, c_uint)> for Input
where D: Into<DeviceType>,
impl<D> TryFrom<(D, c_uint)> for Input
where D: TryInto<DeviceType>,
<D as std::convert::TryInto<DeviceType>>::Error: std::error::Error + Send + Sync + 'static,
{
fn from(pair: (D, c_uint)) -> Self {
type Error = failure::Error;
fn try_from(pair: (D, c_uint)) -> failure::Fallible<Self> {
let (device, id) = pair;
match device.into() {
Ok(match device.try_into()? {
DeviceType::None => Input::None(id),
DeviceType::Joypad => Input::Joypad(JoypadButton::try_from(id).expect("Invalid joypad button ID")),
DeviceType::Mouse => Input::Mouse(MouseButton::try_from(id).expect("Invalid mouse button ID")),
DeviceType::Joypad => Input::Joypad(JoypadButton::try_from(id)?),
DeviceType::Mouse => Input::Mouse(MouseButton::try_from(id)?),
DeviceType::Keyboard => Input::Keyboard(id),
DeviceType::LightGun => Input::LightGun(LightGunButton::try_from(id).expect("Invalid lightgun button ID")),
DeviceType::Analog => Input::Analog(AnalogAxis::try_from(id).expect("Invalid analog axis ID")),
DeviceType::Pointer => Input::Pointer(PointerStat::try_from(id).expect("Invalid pointer stat ID")),
}
DeviceType::LightGun => Input::LightGun(LightGunButton::try_from(id)?),
DeviceType::Analog => Input::Analog(AnalogAxis::try_from(id)?),
DeviceType::Pointer => Input::Pointer(PointerStat::try_from(id)?),
})
}
}
type VideoRefreshCb = dyn FnMut(&[u8], c_uint, c_uint, c_uint); // buffer, width, height, pitch
type AudioSampleCb = dyn FnMut(i16, i16); // left, right
type AudioSampleBatchCb = dyn FnMut(&[i16]) -> usize; // pcm16le stereo samples
type InputPollCb = dyn FnMut();
type InputStateCb = dyn FnMut(u32, Input, DeviceIndex) -> i16; // port, device, index
#[derive(TryFromPrimitive)]
#[repr(u32)]
pub enum EnvRotation {
None = 0,
CounterClockwise90 = 1,
CounterClockwise180 = 2,
CounterClockwise270 = 3,
}
#[derive(TryFromPrimitive)]
#[repr(u32)]
pub enum EnvPixelFormat {
XRGB555 = retro_pixel_format_RETRO_PIXEL_FORMAT_0RGB1555,
RGB565 = retro_pixel_format_RETRO_PIXEL_FORMAT_RGB565,
XRGB8888 = retro_pixel_format_RETRO_PIXEL_FORMAT_XRGB8888,
Unknown = retro_pixel_format_RETRO_PIXEL_FORMAT_UNKNOWN,
}
pub struct EnvVariable {
key: &'static str,
description: &'static str,
expected_values: Vec<&'static str>,
value: Option<String>,
}
#[derive(TryFromPrimitive)]
#[repr(u32)]
pub enum EnvLanguage {
English = retro_language_RETRO_LANGUAGE_ENGLISH,
Japanese = retro_language_RETRO_LANGUAGE_JAPANESE,
French = retro_language_RETRO_LANGUAGE_FRENCH,
Spanish = retro_language_RETRO_LANGUAGE_SPANISH,
German = retro_language_RETRO_LANGUAGE_GERMAN,
Italian = retro_language_RETRO_LANGUAGE_ITALIAN,
Dutch = retro_language_RETRO_LANGUAGE_DUTCH,
PortugueseBrazil = retro_language_RETRO_LANGUAGE_PORTUGUESE_BRAZIL,
PortuguesePortugal = retro_language_RETRO_LANGUAGE_PORTUGUESE_PORTUGAL,
Russian = retro_language_RETRO_LANGUAGE_RUSSIAN,
Korean = retro_language_RETRO_LANGUAGE_KOREAN,
ChineseTraditional = retro_language_RETRO_LANGUAGE_CHINESE_TRADITIONAL,
ChineseSimplified = retro_language_RETRO_LANGUAGE_CHINESE_SIMPLIFIED,
Esperanto = retro_language_RETRO_LANGUAGE_ESPERANTO,
Polish = retro_language_RETRO_LANGUAGE_POLISH,
Vietnamese = retro_language_RETRO_LANGUAGE_VIETNAMESE,
Arabic = retro_language_RETRO_LANGUAGE_ARABIC,
Greek = retro_language_RETRO_LANGUAGE_GREEK,
Last = retro_language_RETRO_LANGUAGE_LAST,
}
// TODO: lifetime
// TODO: experimental calls
pub enum EnvironmentCmdData {
SetRotation(EnvRotation),
GetOverscan(&'static mut bool),
GetCanDupe(&'static mut bool),
SetMessage(retro_message), // TODO
Shutdown,
SetPerformanceLevel(c_uint),
GetSystemDirectory(&'static mut PathBuf),
SetPixelFormat(EnvPixelFormat),
SetInputDescriptors(&'static [retro_input_descriptor]), // TODO
SetKeyboardCallback(retro_keyboard_callback), // TODO
SetDiskControlInterface(retro_disk_control_callback), // TODO
SetHwRender(retro_hw_render_callback), // TODO
GetVariable(&'static mut EnvVariable),
SetVariables(&'static [EnvVariable]),
GetVariableUpdate(&'static mut bool),
SetSupportNoGame(bool),
GetLibretroPath(&'static mut PathBuf),
SetFrameTimeCallback(retro_frame_time_callback), // TODO
SetAudioCallback(retro_audio_callback), // TODO
GetRumbleInterface(&'static mut retro_rumble_interface), // TODO
GetInputDeviceCapabilities(&'static mut u64), // TODO
GetSensorInterface(&'static mut retro_sensor_interface), // TODO
GetCameraInterface(&'static mut retro_camera_callback), // TODO
GetLogInterface(&'static mut retro_log_callback), // TODO
GetPerfInterface(&'static mut retro_perf_callback), // TODO
GetLocationInterface(&'static mut retro_location_callback), // TODO
GetCoreAssetsDirectory(&'static mut PathBuf),
GetSaveDirectory(&'static mut PathBuf),
SetSystemAvInfo(retro_system_av_info),
SetProcAddressCallback(retro_get_proc_address_interface), // TODO
SetSubsystemInfo(retro_subsystem_info), // TODO
SetControllerInfo(retro_controller_info), // TODO
SetMemoryMaps(retro_memory_map), // TODO
SetGeometry(retro_game_geometry), // TODO
GetUsername(&'static mut String),
GetLanguage(&'static mut EnvLanguage),
SetSerializationQuirks(&'static mut u64), // TODO
}
pub trait HandlesEnvironment: Send + Sync + 'static {
// TODO: wrap cmd+data in enum
fn environment(&mut self, cmd: u32, data: *mut c_void) -> bool;
}
pub trait HandlesVideoRefresh: Send + Sync + 'static {
fn video_refresh(&mut self, data: &[u8], width: c_uint, height: c_uint, pitch: c_uint);
}
pub trait HandlesAudioSample: Send + Sync + 'static {
fn audio_sample(&mut self, left: i16, right: i16);
}
pub trait HandlesAudioSampleBatch: Send + Sync + 'static {
fn audio_sample_batch(&mut self, stereo_pcm: &[i16]) -> usize;
}
pub trait HandlesInputPoll: Send + Sync + 'static {
fn input_poll(&mut self);
}
pub trait HandlesInputState: Send + Sync + 'static {
fn input_state(&mut self, port: u32, device: Input, index: DeviceIndex) -> i16;
}

View File

@ -5,16 +5,9 @@ use std::path::Path;
use failure::Fallible;
use libloading;
use crate::libretro_types::*;
use crate::libretro_ffi::*;
use crate::libretro_convert::*;
struct StaticCallbacks {
}
impl StaticCallbacks {
}
pub struct LibretroApi<'a> {
pub retro_set_environment:
libloading::Symbol<'a, unsafe extern "C" fn(arg1: retro_environment_t)>,

163
src/libretro_wrapper.rs Normal file
View File

@ -0,0 +1,163 @@
use core::convert::TryInto;
use core::ffi::c_void;
use core::slice::from_raw_parts;
use std::os::raw::c_uint;
use crate::libretro_convert::*;
use crate::libretro_loading::*;
static mut CB_SINGLETON: StaticCallbacks = StaticCallbacks {
environment: None,
video_refresh: None,
audio_sample: None,
audio_sample_batch: None,
input_poll: None,
input_state: None,
};
#[derive(Default)]
struct StaticCallbacks {
environment: Option<&'static mut dyn HandlesEnvironment>,
video_refresh: Option<&'static mut dyn HandlesVideoRefresh>,
audio_sample: Option<&'static mut dyn HandlesAudioSample>,
audio_sample_batch: Option<&'static mut dyn HandlesAudioSampleBatch>,
input_poll: Option<&'static mut dyn HandlesInputPoll>,
input_state: Option<&'static mut dyn HandlesInputState>,
}
unsafe impl Sync for StaticCallbacks {}
impl StaticCallbacks {
extern "C" fn environment_cb(cmd: u32, data: *mut c_void) -> bool {
match unsafe { CB_SINGLETON.environment.as_mut() } {
Some(cb) => cb.environment(cmd, data),
None => false,
}
}
extern "C" fn video_refresh_cb(data: *const c_void, width: c_uint, height: c_uint, pitch: usize) {
if !data.is_null() {
if let Some(cb) = unsafe { CB_SINGLETON.video_refresh.as_mut() } {
let data = data as *const u8;
let len = pitch * (height as usize);
let slice = unsafe { from_raw_parts(data, len) };
cb.video_refresh(slice, width, height, pitch as u32);
}
}
}
extern "C" fn audio_sample_cb(left: i16, right: i16) {
if let Some(cb) = unsafe { CB_SINGLETON.audio_sample.as_mut() } {
cb.audio_sample(left, right);
}
}
extern "C" fn audio_sample_batch_cb(data: *const i16, frames: usize) -> usize {
unsafe {
match CB_SINGLETON.audio_sample_batch.as_mut() {
Some(cb) => match data.is_null() {
true => 0,
false => cb.audio_sample_batch(from_raw_parts(data, frames)),
}
None => 0,
}
}
}
extern "C" fn input_poll_cb() {
unsafe { CB_SINGLETON.input_poll.as_mut().map(|cb| cb.input_poll()) };
}
extern "C" fn input_state_cb(port: c_uint, device: c_uint, index: c_uint, id: c_uint) -> i16 {
match unsafe { CB_SINGLETON.input_state.as_mut() } {
Some(cb) => match ((device, id).try_into(), index.try_into()) {
(Ok(input), Ok(index)) => cb.input_state(port, input, index),
_ => 0,
},
None => 0,
}
}
pub(crate) fn set_environment(cb: &'static mut dyn HandlesEnvironment) {
unsafe { CB_SINGLETON.environment.replace(cb); }
}
pub(crate) fn set_video_refresh(cb: &'static mut dyn HandlesVideoRefresh) {
unsafe { CB_SINGLETON.video_refresh.replace(cb); }
}
pub(crate) fn set_audio_sample(cb: &'static mut dyn HandlesAudioSample) {
unsafe { CB_SINGLETON.audio_sample.replace(cb); }
}
pub(crate) fn set_audio_sample_batch(cb: &'static mut dyn HandlesAudioSampleBatch) {
unsafe { CB_SINGLETON.audio_sample_batch.replace(cb); }
}
pub(crate) fn set_input_poll(cb: &'static mut dyn HandlesInputPoll) {
unsafe { CB_SINGLETON.input_poll.replace(cb); }
}
pub(crate) fn set_input_state(cb: &'static mut dyn HandlesInputState) {
unsafe { CB_SINGLETON.input_state.replace(cb); }
}
pub(crate) fn reset() {
unsafe {
CB_SINGLETON.environment.take();
CB_SINGLETON.video_refresh.take();
CB_SINGLETON.audio_sample.take();
CB_SINGLETON.audio_sample_batch.take();
CB_SINGLETON.input_poll.take();
CB_SINGLETON.input_state.take();
}
}
}
pub struct LibretroWrapper<'a> {
api: LibretroApi<'a>,
}
impl<'a> From<LibretroApi<'a>> for LibretroWrapper<'a> {
fn from(api: LibretroApi<'a>) -> Self {
api.set_environment(Some(StaticCallbacks::environment_cb));
api.set_video_refresh(Some(StaticCallbacks::video_refresh_cb));
api.set_audio_sample(Some(StaticCallbacks::audio_sample_cb));
api.set_audio_sample_batch(Some(StaticCallbacks::audio_sample_batch_cb));
api.set_input_poll(Some(StaticCallbacks::input_poll_cb));
api.set_input_state(Some(StaticCallbacks::input_state_cb));
LibretroWrapper {
api,
}
}
}
impl<'a> AsRef<LibretroApi<'a>> for LibretroWrapper<'a> {
fn as_ref(&self) -> &LibretroApi<'a> {
&self.api
}
}
impl<'a> Drop for LibretroWrapper<'a> {
fn drop(&mut self) {
StaticCallbacks::reset();
}
}
// a note on lifetimes: we explicitly lie about them here because as long as they live as long as
// the library wrapper itself we're good (we wipe our 'static references on drop() too)
impl<'a> LibretroWrapper<'a> {
pub fn register_environment_handler(&self, handler: &'a mut dyn HandlesEnvironment) {
let ptr: *mut dyn HandlesEnvironment = handler;
StaticCallbacks::set_environment(unsafe { ptr.as_mut() }.unwrap());
}
pub fn register_video_refresh_handler(&self, handler: &'a mut dyn HandlesVideoRefresh) {
let ptr: *mut dyn HandlesVideoRefresh = handler;
StaticCallbacks::set_video_refresh(unsafe { ptr.as_mut() }.unwrap());
}
pub fn register_audio_sample_handler(&self, handler: &'a mut dyn HandlesAudioSample) {
let ptr: *mut dyn HandlesAudioSample = handler;
StaticCallbacks::set_audio_sample(unsafe { ptr.as_mut() }.unwrap());
}
pub fn register_audio_sample_batch_handler(&self, handler: &'a mut dyn HandlesAudioSampleBatch) {
let ptr: *mut dyn HandlesAudioSampleBatch = handler;
StaticCallbacks::set_audio_sample_batch(unsafe { ptr.as_mut() }.unwrap());
}
pub fn register_input_poll_handler(&self, handler: &'a mut dyn HandlesInputPoll) {
let ptr: *mut dyn HandlesInputPoll = handler;
StaticCallbacks::set_input_poll(unsafe { ptr.as_mut() }.unwrap());
}
pub fn register_input_state_handler(&self, handler: &'a mut dyn HandlesInputState) {
let ptr: *mut dyn HandlesInputState = handler;
StaticCallbacks::set_input_state(unsafe { ptr.as_mut() }.unwrap());
}
}

View File

@ -1,19 +1,56 @@
extern crate failure;
extern crate libloading;
use std::ffi::CStr;
#[allow(non_camel_case_types, non_upper_case_globals, non_snake_case, dead_code)]
mod libretro_types;
mod libretro_ffi;
mod libretro_loading;
mod libretro_convert;
mod libretro_wrapper;
use std::ffi::{CStr, c_void};
use std::path::PathBuf;
use structopt::StructOpt;
use crate::libretro_convert::{HandlesEnvironment, HandlesVideoRefresh};
struct Foo;
impl HandlesEnvironment for Foo {
fn environment(&mut self, cmd: u32, data: *mut c_void) -> bool {
println!("environment cmd: {}", cmd);
false
}
}
impl HandlesVideoRefresh for Foo {
fn video_refresh(&mut self, data: &[u8], width: u32, height: u32, pitch: u32) {
println!("video_refresh {}x{}", width, height);
}
}
#[derive(StructOpt)]
struct Opt {
/// Core module to use.
#[structopt(short, long, parse(from_os_str))]
core: PathBuf,
/// Rom to load using the core.
#[structopt(short, long, parse(from_os_str))]
rom: PathBuf,
}
fn main() -> failure::Fallible<()> {
let lib = libloading::Library::new("/home/lifning/.config/retroarch/cores/gambatte_libretro.so")?;
let opt: Opt = Opt::from_args();
let lib = libloading::Library::new(&opt.core)?;
let retro = libretro_loading::LibretroApi::from_library(&lib)?;
unsafe {
println!("api version: {}", (&retro.retro_api_version)());
println!("name: {}", CStr::from_ptr(retro.get_system_info().library_name).to_string_lossy());
}
let wrapper = libretro_wrapper::LibretroWrapper::from(retro);
let mut foo = Foo{};
wrapper.register_environment_handler(&mut foo);
wrapper.register_video_refresh_handler(&mut foo);
println!("api version: {}", wrapper.as_ref().api_version());
println!("name: {}", unsafe { CStr::from_ptr(wrapper.as_ref().get_system_info().library_name) }.to_string_lossy());
wrapper.as_ref().init();
wrapper.as_ref().load_game(Some(&opt.rom), None, None);
wrapper.as_ref().run();
Ok(())
}