WIP idiomatic wrapping of lower-level FFI
This commit is contained in:
parent
0d0dda356c
commit
5b27d27fdf
|
@ -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
|
||||
|
|
2
build.rs
2
build.rs
|
@ -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!");
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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)>,
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
53
src/main.rs
53
src/main.rs
|
@ -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(())
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue