@@ -27,12 +27,14 @@ use std::{
2727 sync:: { Arc , atomic:: AtomicU64 } ,
2828} ;
2929
30- use wincode:: { SchemaWrite , config:: DefaultConfig } ;
30+ use wincode:: { SchemaWrite , Serialize as _ , config:: DefaultConfig } ;
3131
32- use super :: {
33- gate:: { EnterError , Gate , GateGuard } ,
34- shm_io:: { self , AsRawSlice , ShmReader , ShmWriter } ,
35- } ;
32+ use self :: shm_io:: { ShmReader , ShmWriter } ;
33+ use super :: gate:: { EnterError , Gate , GateGuard } ;
34+
35+ mod shm_io;
36+
37+ pub use shm_io:: AsRawSlice ;
3638
3739/// Bytes reserved in front of the arena for the gate word.
3840const GATE_REGION_SIZE : usize = 64 ;
@@ -125,29 +127,45 @@ impl<M: AsRawSlice> GatedShmWriter<M> {
125127 /// [`ClaimError::Capacity`] when the arena cannot fit the frame.
126128 pub fn claim_frame ( & self , size : NonZeroUsize ) -> Result < FrameMut < ' _ > , ClaimError > {
127129 let gate = self . gate ( ) . enter ( ) ?;
128- let frame = self . arena . claim_frame ( size) . ok_or ( ClaimError :: Capacity ) ?;
129- Ok ( FrameMut { frame , _gate : gate } )
130+ let content = self . arena . claim_frame ( size) . ok_or ( ClaimError :: Capacity ) ?;
131+ Ok ( FrameMut { content , _gate : gate } )
130132 }
131133
132134 /// Appends an encoded value as a single frame.
133135 ///
134136 /// # Errors
135137 ///
136138 /// Returns [`ClaimError::Closed`] wrapped in [`WriteEncodedError::Claim`]
137- /// once the read end has closed the gate, and otherwise whatever the arena
138- /// reports.
139+ /// once the read end has closed the gate, [`WriteEncodedError::ZeroSizedFrame`]
140+ /// for a value that encodes to nothing, and otherwise the encoder's error.
141+ ///
142+ /// # Panics
143+ ///
144+ /// Panics if the encoder does not fill exactly the size it asked for, which
145+ /// would mean the schema is inconsistent with itself.
139146 pub fn write_encoded < T : SchemaWrite < DefaultConfig , Src = T > > (
140147 & self ,
141148 value : & T ,
142149 ) -> Result < ( ) , WriteEncodedError > {
143- // The guard is held across the inner write and released only afterwards,
144- // so the frame is complete before the gate count drops.
145- let _gate = self . gate ( ) . enter ( ) . map_err ( ClaimError :: from) ?;
146- self . arena . write_encoded ( value) . map_err ( |error| match error {
147- shm_io:: WriteEncodedError :: EncodeError ( error) => WriteEncodedError :: Encode ( error) ,
148- shm_io:: WriteEncodedError :: ZeroSizedFrame => WriteEncodedError :: ZeroSizedFrame ,
149- shm_io:: WriteEncodedError :: InsufficientSpace => ClaimError :: Capacity . into ( ) ,
150- } )
150+ // Enter the gate before touching the value, so a post-close call
151+ // reports `Claim(Closed)` no matter what the value encodes to.
152+ let gate = self . gate ( ) . enter ( ) . map_err ( ClaimError :: from) ?;
153+
154+ let serialized_size =
155+ usize:: try_from ( T :: serialized_size ( value) ?) . expect ( "serialized size exceeds usize" ) ;
156+ let Some ( size) = NonZeroUsize :: new ( serialized_size) else {
157+ return Err ( WriteEncodedError :: ZeroSizedFrame ) ;
158+ } ;
159+
160+ let content = self . arena . claim_frame ( size) . ok_or ( ClaimError :: Capacity ) ?;
161+ // The frame holds the gate guard, so it is released only after the
162+ // content is written.
163+ let mut frame = FrameMut { content, _gate : gate } ;
164+ let mut writer: & mut [ u8 ] = & mut frame;
165+ T :: serialize_into ( & mut writer, value) ?;
166+ assert_eq ! ( writer. len( ) , 0 ) ;
167+
168+ Ok ( ( ) )
151169 }
152170
153171 fn gate ( & self ) -> Gate < ' _ > {
@@ -158,24 +176,25 @@ impl<M: AsRawSlice> GatedShmWriter<M> {
158176/// An exclusively owned frame together with the gate guard that keeps the region
159177/// open while it is written.
160178///
161- /// The frame is declared first so that its completion lands before the gate is
162- /// released.
179+ /// Dropping it releases the gate, which is what publishes the content to a
180+ /// reader; the arena's own header was already written when the frame was
181+ /// claimed.
163182pub struct FrameMut < ' a > {
164- frame : shm_io :: FrameMut < ' a > ,
183+ content : & ' a mut [ u8 ] ,
165184 _gate : GateGuard < ' a > ,
166185}
167186
168187impl Deref for FrameMut < ' _ > {
169188 type Target = [ u8 ] ;
170189
171190 fn deref ( & self ) -> & Self :: Target {
172- & self . frame
191+ self . content
173192 }
174193}
175194
176195impl DerefMut for FrameMut < ' _ > {
177196 fn deref_mut ( & mut self ) -> & mut Self :: Target {
178- & mut self . frame
197+ self . content
179198 }
180199}
181200
@@ -407,6 +426,18 @@ mod tests {
407426 assert ! ( reader. iter_frames( ) . all( |frame| frame == b"filler" ) ) ;
408427 }
409428
429+ #[ test]
430+ fn write_encoded_after_close_reports_closed_for_any_value ( ) {
431+ let ( writer, receiver) = endpoints ( 1024 ) ;
432+ let _frames = receiver. close ( ) . unwrap ( ) ;
433+
434+ // A zero-sized value must still report the close, not its own size.
435+ assert ! ( matches!(
436+ writer. write_encoded( & ( ) ) ,
437+ Err ( WriteEncodedError :: Claim ( ClaimError :: Closed ) )
438+ ) ) ;
439+ }
440+
410441 #[ test]
411442 fn write_encoded_roundtrips_through_the_gate ( ) {
412443 let ( writer, receiver) = endpoints ( 1024 ) ;
@@ -452,4 +483,162 @@ mod tests {
452483 assert_eq ! ( seen. len( ) , WRITERS * PER_WRITER ) ;
453484 assert_eq ! ( seen[ 0 ] , "0 0" ) ;
454485 }
486+
487+ /// Writers that run out of room must fail cleanly rather than corrupt the
488+ /// arena for the ones that fitted.
489+ #[ test]
490+ fn concurrent_writers_exceeding_the_arena ( ) {
491+ let ( writer, receiver) = endpoints ( 1024 ) ;
492+
493+ thread:: scope ( |scope| {
494+ for _ in 0 ..4 {
495+ scope. spawn ( || {
496+ for _ in 0 ..10 {
497+ let _ = write_frame ( & writer, b"hello" ) ;
498+ let _ = write_frame ( & writer, b"foo" ) ;
499+ let _ = write_frame ( & writer, b"this is a test" ) ;
500+ }
501+ } ) ;
502+ }
503+ } ) ;
504+
505+ let reader = receiver. close ( ) . unwrap ( ) ;
506+ let mut count = 0 ;
507+ for frame in reader. iter_frames ( ) {
508+ count += 1 ;
509+ assert ! (
510+ frame == b"hello" || frame == b"foo" || frame == b"this is a test" ,
511+ "unexpected frame {:?}" ,
512+ from_utf8( frame)
513+ ) ;
514+ }
515+ assert ! ( count > 20 , "expected most writes to fit, got {count}" ) ;
516+ }
517+
518+ /// Several threads racing for the last few bytes must all agree on who got
519+ /// them, and the reader must walk whatever survived without panicking.
520+ #[ test]
521+ fn concurrent_writers_racing_for_the_last_bytes ( ) {
522+ let ( writer, receiver) = endpoints ( GATE_REGION_SIZE + 200 ) ;
523+
524+ thread:: scope ( |scope| {
525+ for _ in 0 ..10 {
526+ scope. spawn ( || {
527+ let _ = write_frame (
528+ & writer,
529+ b"this_is_a_moderately_long_frame_that_might_cause_races" ,
530+ ) ;
531+ } ) ;
532+ }
533+ } ) ;
534+
535+ let reader = receiver. close ( ) . unwrap ( ) ;
536+ let count = reader. iter_frames ( ) . count ( ) ;
537+ // Some but not all of them fit into 200 bytes of arena.
538+ assert ! ( count > 0 ) ;
539+ assert ! ( count < 10 ) ;
540+ }
541+
542+ /// A frame larger than the size header can describe is refused, and the
543+ /// arena stays usable afterwards.
544+ #[ test]
545+ fn a_frame_too_large_for_the_header_is_refused ( ) {
546+ let ( writer, receiver) = endpoints ( 1024 ) ;
547+
548+ let oversized = NonZeroUsize :: new ( usize:: try_from ( u32:: MAX ) . unwrap ( ) + 1 ) . unwrap ( ) ;
549+ assert ! ( matches!( writer. claim_frame( oversized) , Err ( ClaimError :: Capacity ) ) ) ;
550+
551+ write_frame ( & writer, b"test" ) . unwrap ( ) ;
552+ let reader = receiver. close ( ) . unwrap ( ) ;
553+ assert_eq ! ( reader. iter_frames( ) . collect:: <Vec <_>>( ) , vec![ b"test" . as_slice( ) ] ) ;
554+ }
555+
556+ #[ test]
557+ fn a_misaligned_region_is_rejected ( ) {
558+ struct Misaligned ( MockRegion ) ;
559+
560+ impl AsRawSlice for Misaligned {
561+ fn as_raw_slice ( & self ) -> * mut [ u8 ] {
562+ let raw_slice = self . 0 . as_raw_slice ( ) ;
563+ slice_from_raw_parts_mut (
564+ // SAFETY: shifting by one byte deliberately misaligns the
565+ // region and stays inside the over-allocated backing buffer.
566+ unsafe { raw_slice. cast :: < u8 > ( ) . add ( 1 ) } ,
567+ raw_slice. len ( ) - 1 ,
568+ )
569+ }
570+ }
571+
572+ let misaligned = Misaligned ( MockRegion :: alloc ( 1024 ) ) ;
573+ assert_ne ! ( misaligned. as_raw_slice( ) . cast:: <u8 >( ) as usize % align_of:: <u64 >( ) , 0 ) ;
574+
575+ let result = std:: panic:: catch_unwind ( || {
576+ // SAFETY: the region is deliberately misaligned to check that the
577+ // constructor catches it; the assertion fires before any access.
578+ unsafe { GatedShmWriter :: new ( misaligned) } ;
579+ } ) ;
580+
581+ assert ! ( result. is_err( ) , "a misaligned region must be rejected" ) ;
582+ }
583+
584+ /// The same protocol over a real shared mapping, written by several
585+ /// processes at once.
586+ #[ test]
587+ #[ cfg( not( miri) ) ]
588+ fn real_shm_across_processes ( ) {
589+ use std:: process:: { Child , Command } ;
590+
591+ use rustc_hash:: FxHashSet ;
592+ use subprocess_test:: command_for_fn;
593+
594+ const CHILD_COUNT : usize = 12 ;
595+ const FRAME_COUNT_EACH_CHILD : usize = 100 ;
596+ const SHM_SIZE : usize = 1024 * 1024 ;
597+
598+ let ( keeper, handle) = fspy_shm:: create ( SHM_SIZE ) . unwrap ( ) ;
599+ let shm_id = keeper. id ( ) . to_str ( ) . expect ( "test temp dir is UTF-8" ) . to_owned ( ) ;
600+ let mapping = handle. map ( ) . unwrap ( ) ;
601+ // SAFETY: the backing file was just created zero-initialized, the
602+ // mapping stays valid while the receiver holds it, and only this
603+ // protocol reaches it.
604+ let receiver = unsafe { GatedShmReceiver :: new ( mapping) } ;
605+
606+ let children: Vec < Child > = ( 0 ..CHILD_COUNT )
607+ . map ( |child_index| {
608+ let cmd = command_for_fn ! ( ( shm_id. clone( ) , child_index) , |(
609+ shm_id,
610+ child_index,
611+ ) : (
612+ String ,
613+ usize
614+ ) | {
615+ let mapping =
616+ fspy_shm:: open( std:: ffi:: OsStr :: new( & shm_id) ) . unwrap( ) . map( ) . unwrap( ) ;
617+ // SAFETY: the mapping was created under the same
618+ // contract and only this protocol reaches it.
619+ let writer = unsafe { GatedShmWriter :: new( mapping) } ;
620+ for i in 0 ..FRAME_COUNT_EACH_CHILD {
621+ let frame_data = format!( "{child_index} {i}" ) ;
622+ let size = NonZeroUsize :: new( frame_data. len( ) ) . unwrap( ) ;
623+ writer. claim_frame( size) . unwrap( ) . copy_from_slice( frame_data. as_bytes( ) ) ;
624+ }
625+ } ) ;
626+ Command :: from ( cmd) . spawn ( ) . unwrap ( )
627+ } )
628+ . collect ( ) ;
629+
630+ for mut child in children {
631+ assert ! ( child. wait( ) . unwrap( ) . success( ) ) ;
632+ }
633+
634+ let reader = receiver. close ( ) . unwrap ( ) ;
635+ let frames: FxHashSet < & str > =
636+ reader. iter_frames ( ) . map ( |frame| from_utf8 ( frame) . unwrap ( ) ) . collect ( ) ;
637+ assert_eq ! ( frames. len( ) , CHILD_COUNT * FRAME_COUNT_EACH_CHILD ) ;
638+ for child_index in 0 ..CHILD_COUNT {
639+ for i in 0 ..FRAME_COUNT_EACH_CHILD {
640+ assert ! ( frames. contains( format!( "{child_index} {i}" ) . as_str( ) ) ) ;
641+ }
642+ }
643+ }
455644}
0 commit comments