After implementing a parallel function to analyse multiple deals simultaneously (AnalyseAllPlaysBin, link to the docs) and writing a little test for it, cargo test started failing with:
exit code: 0xc0000409, STATUS_STACK_BUFFER_OVERRUN
or with:
exit code: 0xc0000005, STATUS_ACCESS_VIOLATION
note: test exited abnormally; to see the full output pass --nocapture to the harness.
Segmentation fault
I was a bit surprised and I dug a bit to understand the problem. I found out that the reason for the failure was cargo running the tests in parallel, and I think simultaneous calls mess up with the DLL, making it segfault.
Using cargo t -- --test-threads=1 solves the problem, but since I'm writing a library I hope to use for an application in the future, I would like to understand the problem and solve it in some way. I'm not planning to make multiple simultaneous calls to the DLL but the problem still bothers me.
I cannot provide an MWE but I'll give some code for reference:
use dds::{
deal, // The `deal` type the C++ library uses
solvedPlays, // Type of the C++ library
solvedPlay, // Same as above
playTraceBin, // Same as above
playTracesBin, // Same as above
PlayTraceBin, // Cards played for the deal to analyze
PlayTracesBin, // Collection of PlayTraceBin for different deals
SolvedPlays, // My wrapper around the C++ type
SolvedPlay, // Same
RankSeq, // Sequence of the rank (2,3,Q,K,A ecc.) of cards played
RawDDSRef, // Trait (fn get_raw(&self)) for getting a ref to the underlying struct of the Rust wrapper types
RawDDSRefMut, // Same as above, but mut (fn get_raw_mut(&mut self))
AsRawDDS, // Same as above, but not a ref (fn as_raw(self))
SuitSeq, // Sequence of suits of cards played
Target,Mode,Solutions // Parameters used by the DLL
MAXNOOFBOARDS,
};
const TRIES: usize = 200;
const CHUNK_SIZE: i32 = 10;
pub fn initialize_test() -> DealMock {
DealMock {
hands: [
[8712, 256114688, 2199023255552, 2344123606046343168],
[22528, 10485760, 79182017069056, 744219838422974464],
[484, 1612185600, 1924145348608, 4611686018427387904],
[1040, 268435456, 57415122812928, 1522216674051227648],
],
}
}
pub trait PlayAnalyzer {
/// Analyzes a single hand
/// # Errors
/// Will return an Error when DDS fails in some way.
fn analyze_play(
deal: &D,
contract: &C,
play: PlayTraceBin,
) -> Result;
/// Analyzes a bunch of hands in paraller.
/// # Errors
/// Will return an Error when DDS fails in some way or the deals and contracts vecs have
/// different length or their length doe
fn analyze_all_plays(
deals: Vec,
contracts: Vec,
plays: &mut PlayTracesBin,
) -> Result;
}
impl PlayAnalyzer for DDSPlayAnalyzer {
#[inline]
fn analyze_all_plays(
deals: Vec,
contracts: Vec,
plays: &mut PlayTracesBin,
) -> Result {
let deals_len = i32::try_from(deals.len().clamp(0, MAXNOOFBOARDS)).unwrap();
let contracts_len = i32::try_from(contracts.len().clamp(0, MAXNOOFBOARDS)).unwrap();
if deals_len != contracts_len || deals_len == 0 || contracts_len == 0 {
return Err(RETURN_UNKNOWN_FAULT.into()); // The error tells that
// either something went terribly wrong or we used wrongly sized inputs.
}
let mut c_deals: Vec = contracts
.into_iter()
.zip(deals)
.map(|(contract, deal)| construct_dds_deal(contract, deal))
.collect();
c_deals.resize(
MAXNOOFBOARDS,
deal {
trump: -1,
first: -1,
currentTrickSuit: [-1i32; 3],
currentTrickRank: [-1i32; 3],
remainCards: [[0u32; 4]; 4],
},
);
let mut boards = boards {
noOfBoards: deals_len,
// We know vec has the right length
deals: match c_deals.try_into().unwrap(),
target: [Target::MaxTricks.into(); MAXNOOFBOARDS],
solutions: [Solutions::Best.into(); MAXNOOFBOARDS],
mode: [Mode::Auto.into(); MAXNOOFBOARDS],
};
let mut solved_plays = SolvedPlays {
solved_plays: solvedPlays {
noOfBoards: deals_len,
solved: [solvedPlay::new(); MAXNOOFBOARDS],
},
};
let bop: *mut boards = &mut boards;
let solved: *mut solvedPlays = solved_plays.get_raw_mut();
let play_trace: *mut playTracesBin = (*plays).get_raw_mut();
// SAFETY: calling C
let result = unsafe { AnalyseAllPlaysBin(bop, play_trace, solved, CHUNK_SIZE) };
match result {
// RETURN_NO_FAULT == 1i32
1i32 => Ok(solved_plays),
n => Err(n.into()),
}
}
#[inline]
fn analyze_play(
deal: &D,
contract: &C,
play: PlayTraceBin,
) -> Result {
let c_deal = construct_dds_deal(contract, deal);
let mut solved_play = SolvedPlay::new();
let solved: *mut solvedPlay = &mut solved_play.solved_play;
let play_trace = play.as_raw();
// SAFETY: calling an external C function
let result = unsafe { AnalysePlayBin(c_deal, play_trace, solved, 0) };
match result {
1i32 => Ok(solved_play),
n => Err(n.into()),
}
}
}
/// Constructs a DDS deal from a DDS contract and a DDS deal representation
fn construct_dds_deal(contract: &C, deal: &D) -> deal {
let (trump, first) = contract.as_dds_contract();
deal {
trump,
first,
currentTrickSuit: [0i32; 3],
currentTrickRank: [0i32; 3],
remainCards: deal.as_dds_deal().as_slice(),
}
}
#[test]
fn analyse_play_test() {
let deal = initialize_test();
let contract = ContractMock {};
let suitseq = SuitSeq::try_from([0i32, 0i32, 0i32, 0i32]).unwrap();
let rankseq = RankSeq::try_from([4i32, 3i32, 12i32, 2i32]).unwrap();
let play = PlayTraceBin::new(suitseq, rankseq);
let solvedplay = DDSPlayAnalyzer::analyze_play(&deal, &contract, play).unwrap();
assert_eq!([2, 2, 2, 2, 2], solvedplay.solved_play.tricks[..5]);
}
#[test]
fn analyse_all_play_test() {
let mut deals_owner = Vec::with_capacity(TRIES);
deals_owner.resize_with(TRIES, initialize_test);
let deals = deals_owner.iter().collect();
let suitseq = SuitSeq::try_from([0, 0, 0, 0]).unwrap();
let rankseq = RankSeq::try_from([4, 3, 12, 2]).unwrap();
let mut suitseqs = Vec::with_capacity(TRIES);
let mut rankseqs = Vec::with_capacity(TRIES);
suitseqs.resize_with(TRIES, || suitseq.clone());
rankseqs.resize_with(TRIES, || rankseq.clone());
let contracts_owner = Vec::from([ContractMock {}; TRIES]);
let contracts = contracts_owner.iter().collect();
let mut plays = PlayTracesBin::from_sequences(suitseqs, rankseqs).unwrap();
let solved_plays = DDSPlayAnalyzer::analyze_all_plays(deals, contracts, &mut plays).unwrap();
let real_plays = solved_plays.get_raw();
assert_eq!(TRIES, real_plays.noOfBoards.try_into().unwrap());
for plays in real_plays.solved {
assert_eq!([2, 2, 2, 2, 2], plays.tricks[..5]);
}
}
I could wrap the DDSAnalyzer in an Arc and then lock it for the duration of the external function call. I think this should work (didn't have time to try it) but I don't know if it is the correct approach.
I would like to ask two things:
* Why I get those errors in a multithreaded situation?
* Would using a Arc work? Is it the correct solution or should I do something different?
Thanks to everyone!