Skip to content

Commit b5d890a

Browse files
committed
Fix unsoundess in libretro port
1 parent 93431c4 commit b5d890a

File tree

1 file changed

+58
-42
lines changed

1 file changed

+58
-42
lines changed

libretro/src/lib.rs

Lines changed: 58 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use std::{
2+
cell::{Cell, RefCell, RefMut},
23
ffi::{c_char, c_void},
34
io::Write,
45
mem::MaybeUninit,
@@ -53,15 +54,20 @@ macro_rules! c_str {
5354

5455
/// The Core global state.
5556
struct Core {
56-
env_callback: retro_environment_t,
57-
video_callback: retro_video_refresh_t,
58-
audio_callback: retro_audio_sample_batch_t,
59-
input_poll_callback: retro_input_poll_t,
60-
input_state_callback: retro_input_state_t,
61-
state: Option<GameBoy>,
57+
env_callback: Cell<retro_environment_t>,
58+
video_callback: Cell<retro_video_refresh_t>,
59+
audio_callback: Cell<retro_audio_sample_batch_t>,
60+
input_poll_callback: Cell<retro_input_poll_t>,
61+
input_state_callback: Cell<retro_input_state_t>,
62+
state: RefCell<Option<GameBoy>>,
6263

6364
// A double buffer of the lcd screen pixels. Updated on vblank.
64-
screen_buffer: [u8; SCREEN_WIDTH as usize * SCREEN_HEIGHT as usize],
65+
screen_buffer: RefCell<[u8; SCREEN_WIDTH as usize * SCREEN_HEIGHT as usize]>,
66+
67+
#[cfg(debug_assertions)]
68+
/// Use to assert that the libretro API is single-threaded. (Maybe libretro can call the API
69+
/// from multiple threads, but let's assume it doesn't)
70+
owner_thread: std::thread::ThreadId,
6571
}
6672
impl Default for Core {
6773
fn default() -> Self {
@@ -72,24 +78,32 @@ impl Default for Core {
7278
input_poll_callback: Default::default(),
7379
input_state_callback: Default::default(),
7480
state: Default::default(),
75-
screen_buffer: [0; SCREEN_WIDTH as usize * SCREEN_HEIGHT as usize],
81+
screen_buffer: [0; SCREEN_WIDTH as usize * SCREEN_HEIGHT as usize].into(),
82+
83+
#[cfg(debug_assertions)]
84+
owner_thread: std::thread::current().id(),
7685
}
7786
}
7887
}
88+
impl Core {
89+
fn state_mut(&self) -> RefMut<GameBoy> {
90+
RefMut::map(self.state.borrow_mut(), |x| x.as_mut().unwrap())
91+
}
92+
}
7993

8094
static mut CORE: Option<Core> = None;
8195

82-
fn core() -> &'static mut Core {
83-
// RetroLib promises that it will not call the API from multiple threads, so this is safe, right?
84-
// Actually this is unsafe because the caller could leak the mutable reference. This should be a
85-
// RefCell or something.
86-
// In fact, I am already doing UB, because I am calling core() in retro_run and in the
87-
// v_blank_callback, creating two mutable references to CORE at the same time!
96+
fn core() -> &'static Core {
97+
// SAFETY: the libretro API is single-threaded, so we can garantee there is no concurrent acess
98+
// to this function. CORE is never mutate after being initialized, so subsequent calls to this
99+
// function will not invalidate previous returned references.
88100
unsafe {
89-
if CORE.is_none() {
90-
CORE = Some(Core::default());
91-
}
92-
CORE.as_mut().unwrap()
101+
let core = CORE.get_or_insert_with(Core::default);
102+
103+
// assert this is single-threaded
104+
debug_assert_eq!(core.owner_thread, std::thread::current().id());
105+
106+
core
93107
}
94108
}
95109

@@ -100,7 +114,7 @@ pub extern "C" fn retro_api_version() -> u32 {
100114

101115
#[no_mangle]
102116
pub extern "C" fn retro_set_environment(callback: retro_environment_t) {
103-
core().env_callback = callback;
117+
core().env_callback.set(callback);
104118
let mut log_callback: retro_log_callback = retro_log_callback { log: None };
105119

106120
if let Some(callback) = callback {
@@ -192,26 +206,27 @@ extern "C" fn retro_load_game(info: Option<&retro_game_info>) -> bool {
192206
let mut gb = GameBoy::new(None, cartridge);
193207
gb.sound.get_mut().sample_frequency = SAMPLE_RATE;
194208
gb.v_blank = Some(Box::new(|gb| {
195-
core().screen_buffer = gb.ppu.get_mut().screen.packed();
209+
*core().screen_buffer.borrow_mut() = gb.ppu.get_mut().screen.packed();
196210
}));
197211

198-
core().state = Some(gb);
212+
*core().state.borrow_mut() = Some(gb);
199213

200214
true
201215
}
202216

203217
#[no_mangle]
204218
pub extern "C" fn retro_run() {
205219
let core = core();
206-
let Some(state) = &mut core.state else {
207-
log::error!("GameBoy is not loaded?");
208-
return;
209-
};
210-
211-
state.joypad = 0xff;
212-
if let (Some(input_poll), Some(input_state)) =
213-
(core.input_poll_callback, core.input_state_callback)
214-
{
220+
// let Some(state) = &mut core.state else {
221+
// log::error!("GameBoy is not loaded?");
222+
// return;
223+
// };
224+
225+
core.state_mut().joypad = 0xff;
226+
if let (Some(input_poll), Some(input_state)) = (
227+
core.input_poll_callback.get(),
228+
core.input_state_callback.get(),
229+
) {
215230
let key_map = [
216231
RETRO_DEVICE_ID_JOYPAD_RIGHT,
217232
RETRO_DEVICE_ID_JOYPAD_LEFT,
@@ -229,18 +244,18 @@ pub extern "C" fn retro_run() {
229244
for (i, id) in key_map.iter().copied().enumerate() {
230245
let value = unsafe { input_state(0, RETRO_DEVICE_JOYPAD, 0, id) };
231246
if value != 0 {
232-
state.joypad &= !(1 << i);
247+
core.state_mut().joypad &= !(1 << i);
233248
}
234249
}
235250
}
236251

237-
let target = state.clock_count + gameroy::consts::FRAME_CYCLES;
238-
while state.clock_count < target {
239-
Interpreter(state).interpret_op();
252+
let target = core.state_mut().clock_count + gameroy::consts::FRAME_CYCLES;
253+
while core.state_mut().clock_count < target {
254+
Interpreter(&mut core.state_mut()).interpret_op();
240255
}
241256

242-
if let Some(callback) = core.video_callback {
243-
let frame = core.screen_buffer.map(|c| {
257+
if let Some(callback) = core.video_callback.get() {
258+
let frame = core.screen_buffer.borrow_mut().map(|c| {
244259
/// 0RGB1555 format
245260
/// FIXME: this format is deprecated
246261
#[allow(clippy::unusual_byte_groupings)]
@@ -262,9 +277,10 @@ pub extern "C" fn retro_run() {
262277
}
263278
}
264279

265-
if let Some(callback) = core.audio_callback {
280+
if let Some(callback) = core.audio_callback.get() {
266281
unsafe {
267-
let buffer = state.sound.get_mut().get_output(state.clock_count);
282+
let clock_count = core.state_mut().clock_count;
283+
let buffer = core.state_mut().sound.get_mut().get_output(clock_count);
268284
let buffer: Vec<i16> = buffer.into_iter().map(|x| (x as i16 - 128) * 30).collect();
269285
(callback)(buffer.as_ptr(), buffer.len() as u64 / 2);
270286
}
@@ -274,25 +290,25 @@ pub extern "C" fn retro_run() {
274290
#[no_mangle]
275291
pub extern "C" fn retro_set_video_refresh(callback: retro_video_refresh_t) {
276292
log::info!("set video refresh");
277-
core().video_callback = callback;
293+
core().video_callback.set(callback);
278294
}
279295

280296
#[no_mangle]
281297
pub extern "C" fn retro_set_audio_sample_batch(callback: retro_audio_sample_batch_t) {
282298
log::info!("set audio sample batch");
283-
core().audio_callback = callback;
299+
core().audio_callback.set(callback);
284300
}
285301

286302
#[no_mangle]
287303
pub extern "C" fn retro_set_input_poll(callback: retro_input_poll_t) {
288304
log::info!("set input poll");
289-
core().input_poll_callback = callback;
305+
core().input_poll_callback.set(callback);
290306
}
291307

292308
#[no_mangle]
293309
pub extern "C" fn retro_set_input_state(callback: retro_input_state_t) {
294310
log::info!("set input state");
295-
core().input_state_callback = callback;
311+
core().input_state_callback.set(callback);
296312
}
297313

298314
#[no_mangle]

0 commit comments

Comments
 (0)