fix(cxxengine): catch errors

This commit is contained in:
Henri Bourcereau 2026-03-02 16:10:30 +01:00
parent 4ea4b1249b
commit c780f8bfe4

View file

@ -9,10 +9,28 @@
//! and events are mirrored back before being applied — exactly as in //! and events are mirrored back before being applied — exactly as in
//! pyengine.rs. //! pyengine.rs.
use std::panic::{self, AssertUnwindSafe};
use crate::dice::Dice; use crate::dice::Dice;
use crate::game::{GameEvent, GameState, Stage, TurnStage}; use crate::game::{GameEvent, GameState, Stage, TurnStage};
use crate::training_common::{get_valid_action_indices, TrictracAction}; use crate::training_common::{get_valid_action_indices, TrictracAction};
/// Catch any Rust panic and convert it to anyhow::Error so it never
/// crosses the C FFI boundary as undefined behaviour.
fn catch_panics<F, T>(f: F) -> anyhow::Result<T>
where
F: FnOnce() -> anyhow::Result<T> + panic::UnwindSafe,
{
panic::catch_unwind(f).unwrap_or_else(|e| {
let msg = e
.downcast_ref::<String>()
.map(|s| s.as_str())
.or_else(|| e.downcast_ref::<&str>().copied())
.unwrap_or("unknown panic payload");
Err(anyhow::anyhow!("Rust panic in FFI: {}", msg))
})
}
// ── cxx bridge declaration ──────────────────────────────────────────────────── // ── cxx bridge declaration ────────────────────────────────────────────────────
#[cxx::bridge(namespace = "trictrac_engine")] #[cxx::bridge(namespace = "trictrac_engine")]
@ -98,7 +116,9 @@ pub fn new_trictrac_engine() -> Box<TricTracEngine> {
let mut game_state = GameState::new(false); // schools_enabled = false let mut game_state = GameState::new(false); // schools_enabled = false
game_state.init_player("player1"); game_state.init_player("player1");
game_state.init_player("player2"); game_state.init_player("player2");
game_state.consume(&GameEvent::BeginGame { goes_first: 1 }); game_state
.consume(&GameEvent::BeginGame { goes_first: 1 })
.expect("BeginGame failed during engine initialization");
Box::new(TricTracEngine { game_state }) Box::new(TricTracEngine { game_state })
} }
@ -127,13 +147,16 @@ impl TricTracEngine {
if player_idx != self.current_player_idx() { if player_idx != self.current_player_idx() {
return Ok(vec![]); return Ok(vec![]);
} }
if player_idx == 0 { catch_panics(AssertUnwindSafe(|| {
get_valid_action_indices(&self.game_state) if player_idx == 0 {
.map(|v| v.into_iter().map(|i| i as u64).collect()) get_valid_action_indices(&self.game_state)
} else { .map(|v| v.into_iter().map(|i| i as u64).collect())
let mirror = self.game_state.mirror(); } else {
get_valid_action_indices(&mirror).map(|v| v.into_iter().map(|i| i as u64).collect()) let mirror = self.game_state.mirror();
} get_valid_action_indices(&mirror)
.map(|v| v.into_iter().map(|i| i as u64).collect())
}
}))
} }
fn action_to_string(&self, player_idx: u64, action_idx: u64) -> String { fn action_to_string(&self, player_idx: u64, action_idx: u64) -> String {
@ -188,38 +211,42 @@ impl TricTracEngine {
let dice = Dice { let dice = Dice {
values: (dice.die1, dice.die2), values: (dice.die1, dice.die2),
}; };
self.game_state catch_panics(AssertUnwindSafe(|| {
.consume(&GameEvent::RollResult { player_id, dice }); self.game_state
Ok(()) .consume(&GameEvent::RollResult { player_id, dice })
.map_err(|e| anyhow::anyhow!(e))
}))
} }
fn apply_action(&mut self, action_idx: u64) -> anyhow::Result<()> { fn apply_action(&mut self, action_idx: u64) -> anyhow::Result<()> {
let needs_mirror = self.game_state.active_player_id == 2; catch_panics(AssertUnwindSafe(|| {
let needs_mirror = self.game_state.active_player_id == 2;
let event = TrictracAction::from_action_index(action_idx as usize).and_then(|a| { let event = TrictracAction::from_action_index(action_idx as usize).and_then(|a| {
let state = if needs_mirror { let state = if needs_mirror {
&self.game_state.mirror() &self.game_state.mirror()
} else { } else {
&self.game_state &self.game_state
}; };
a.to_event(state) a.to_event(state)
.map(|e| if needs_mirror { e.get_mirror(false) } else { e }) .map(|e| if needs_mirror { e.get_mirror(false) } else { e })
}); });
match event { match event {
Some(evt) if self.game_state.validate(&evt) => { Some(evt) if self.game_state.validate(&evt) => self
self.game_state.consume(&evt); .game_state
Ok(()) .consume(&evt)
.map_err(|e| anyhow::anyhow!(e)),
Some(evt) => anyhow::bail!(
"apply_action: event {:?} is not valid in current state {}",
evt,
self.game_state
),
None => anyhow::bail!(
"apply_action: could not build event from action index {}",
action_idx
),
} }
Some(evt) => anyhow::bail!( }))
"apply_action: event {:?} is not valid in current state {}",
evt,
self.game_state
),
None => anyhow::bail!(
"apply_action: could not build event from action index {}",
action_idx
),
}
} }
} }