Skip to main content

rust_hdf5/
swmr.rs

1//! Single Writer / Multiple Reader (SWMR) API.
2//!
3//! Provides a high-level wrapper around the SWMR protocol for streaming
4//! frame-based data (e.g., area detector images).
5
6use std::path::Path;
7
8use crate::format::messages::attribute::AttributeMessage;
9use crate::format::messages::datatype::DatatypeMessage;
10use crate::io::locking::FileLocking;
11use crate::io::Hdf5Reader;
12use crate::io::SwmrWriter as IoSwmrWriter;
13
14use crate::error::{Hdf5Error, Result};
15use crate::types::H5Type;
16
17/// SWMR writer for streaming frame-based data to an HDF5 file.
18///
19/// Usage:
20/// ```no_run
21/// use rust_hdf5::swmr::SwmrFileWriter;
22///
23/// let mut writer = SwmrFileWriter::create("stream.h5").unwrap();
24/// let ds = writer.create_streaming_dataset::<f32>("frames", &[256, 256]).unwrap();
25/// writer.start_swmr().unwrap();
26///
27/// // Write frames
28/// let frame_data = vec![0.0f32; 256 * 256];
29/// let raw: Vec<u8> = frame_data.iter()
30///     .flat_map(|v| v.to_le_bytes())
31///     .collect();
32/// writer.append_frame(ds, &raw).unwrap();
33/// writer.flush().unwrap();
34///
35/// writer.close().unwrap();
36/// ```
37pub struct SwmrFileWriter {
38    inner: IoSwmrWriter,
39}
40
41impl SwmrFileWriter {
42    /// Create a new HDF5 file for SWMR streaming using the env-var-derived
43    /// locking policy.
44    pub fn create<P: AsRef<Path>>(path: P) -> Result<Self> {
45        let inner = IoSwmrWriter::create(path.as_ref())?;
46        Ok(Self { inner })
47    }
48
49    /// Create a new HDF5 file for SWMR streaming with an explicit locking
50    /// policy. The writer holds an exclusive lock until [`Self::start_swmr`]
51    /// is called, at which point the lock is downgraded to shared so
52    /// concurrent SWMR readers can attach.
53    pub fn create_with_locking<P: AsRef<Path>>(path: P, locking: FileLocking) -> Result<Self> {
54        let inner = IoSwmrWriter::create_with_locking(path.as_ref(), locking)?;
55        Ok(Self { inner })
56    }
57
58    /// Reopen a cleanly-closed HDF5 file to resume SWMR streaming.
59    ///
60    /// Existing datasets are reconstructed; locate them with
61    /// [`dataset_index`](Self::dataset_index), call [`start_swmr`](Self::start_swmr)
62    /// to re-enter SWMR mode, then continue with [`append_frame`](Self::append_frame).
63    /// Appending to a multi-frame-chunk dataset (`chunk[0] > 1`) after reopen
64    /// is rejected — its final partial band was zero-padded at the original
65    /// close. Recovering a crashed (never cleanly closed) file is not supported.
66    pub fn open_append<P: AsRef<Path>>(path: P) -> Result<Self> {
67        let inner = IoSwmrWriter::open_append(path.as_ref())?;
68        Ok(Self { inner })
69    }
70
71    /// Reopen a cleanly-closed HDF5 file to resume SWMR streaming with an
72    /// explicit locking policy. See [`Self::open_append`].
73    pub fn open_append_with_locking<P: AsRef<Path>>(path: P, locking: FileLocking) -> Result<Self> {
74        let inner = IoSwmrWriter::open_append_with_locking(path.as_ref(), locking)?;
75        Ok(Self { inner })
76    }
77
78    /// Return the index of a dataset by name, or `None` if absent.
79    ///
80    /// Mainly used after [`open_append`](Self::open_append) to recover the
81    /// index of a reconstructed dataset for [`append_frame`](Self::append_frame).
82    pub fn dataset_index(&self, name: &str) -> Option<usize> {
83        self.inner.dataset_index(name)
84    }
85
86    /// Create a streaming dataset.
87    ///
88    /// The dataset will have shape `[0, frame_dims...]` initially, with
89    /// chunk dimensions `[1, frame_dims...]` and unlimited first dimension.
90    ///
91    /// Returns the dataset index for use with `append_frame`.
92    pub fn create_streaming_dataset<T: H5Type>(
93        &mut self,
94        name: &str,
95        frame_dims: &[u64],
96    ) -> Result<usize> {
97        self.create_streaming_dataset_with_datatype(name, T::hdf5_type(), frame_dims)
98    }
99
100    /// [`Self::create_streaming_dataset`] with an explicit stored datatype instead of a
101    /// carrier `T`, for a type no `T` describes (e.g. a reduced-precision
102    /// type for [`FilterPipeline::nbit`](crate::FilterPipeline::nbit)); an
103    /// element is `datatype.element_size()` bytes.
104    pub fn create_streaming_dataset_with_datatype(
105        &mut self,
106        name: &str,
107        datatype: crate::DatatypeMessage,
108        frame_dims: &[u64],
109    ) -> Result<usize> {
110        let idx = self
111            .inner
112            .create_streaming_dataset(name, datatype, frame_dims)?;
113        Ok(idx)
114    }
115
116    /// Create a streaming dataset whose frames are compressed.
117    ///
118    /// Like [`create_streaming_dataset`](Self::create_streaming_dataset) but
119    /// each appended frame is run through `pipeline` (e.g.
120    /// `FilterPipeline::deflate(4)`). SWMR appends and in-place header
121    /// updates work the same as for uncompressed streaming datasets.
122    pub fn create_streaming_dataset_compressed<T: H5Type>(
123        &mut self,
124        name: &str,
125        frame_dims: &[u64],
126        pipeline: crate::format::messages::filter::FilterPipeline,
127    ) -> Result<usize> {
128        self.create_streaming_dataset_compressed_with_datatype(
129            name,
130            T::hdf5_type(),
131            frame_dims,
132            pipeline,
133        )
134    }
135
136    /// [`Self::create_streaming_dataset_compressed`] with an explicit stored datatype instead of a
137    /// carrier `T`, for a type no `T` describes (e.g. a reduced-precision
138    /// type for [`FilterPipeline::nbit`](crate::FilterPipeline::nbit)); an
139    /// element is `datatype.element_size()` bytes.
140    pub fn create_streaming_dataset_compressed_with_datatype(
141        &mut self,
142        name: &str,
143        datatype: crate::DatatypeMessage,
144        frame_dims: &[u64],
145        pipeline: crate::format::messages::filter::FilterPipeline,
146    ) -> Result<usize> {
147        let idx = self
148            .inner
149            .create_streaming_dataset_compressed(name, datatype, frame_dims, pipeline)?;
150        Ok(idx)
151    }
152
153    /// Create a streaming dataset whose frames are split into fixed-size
154    /// chunk tiles.
155    ///
156    /// `frame_dims` is the per-frame shape (e.g. `[1024, 1024]`);
157    /// `frame_chunk` is the tile shape within a frame (e.g. `[256, 256]`),
158    /// of the same rank. The on-disk chunk shape becomes
159    /// `[1, frame_chunk...]`, so each frame is stored as
160    /// `product(frame_dims / frame_chunk)` chunks instead of one. This
161    /// mirrors area-detector tiling controls such as NDFileHDF5's
162    /// `nRowChunks` / `nColChunks`: it changes only the partial-read
163    /// granularity and compression unit, not the stored data.
164    /// [`append_frame`](Self::append_frame) accepts a whole frame and
165    /// splits it into tiles automatically.
166    pub fn create_streaming_dataset_tiled<T: H5Type>(
167        &mut self,
168        name: &str,
169        frame_dims: &[u64],
170        frame_chunk: &[u64],
171    ) -> Result<usize> {
172        let idx = self.inner.create_streaming_dataset_tiled(
173            name,
174            T::hdf5_type(),
175            frame_dims,
176            frame_chunk,
177        )?;
178        Ok(idx)
179    }
180
181    /// Create a compressed streaming dataset whose frames are split into
182    /// fixed-size chunk tiles. See
183    /// [`create_streaming_dataset_tiled`](Self::create_streaming_dataset_tiled)
184    /// for the meaning of `frame_chunk`; each tile is the compression unit.
185    pub fn create_streaming_dataset_tiled_compressed<T: H5Type>(
186        &mut self,
187        name: &str,
188        frame_dims: &[u64],
189        frame_chunk: &[u64],
190        pipeline: crate::format::messages::filter::FilterPipeline,
191    ) -> Result<usize> {
192        self.create_streaming_dataset_tiled_compressed_with_datatype(
193            name,
194            T::hdf5_type(),
195            frame_dims,
196            frame_chunk,
197            pipeline,
198        )
199    }
200
201    /// [`Self::create_streaming_dataset_tiled_compressed`] with an explicit stored datatype instead of a
202    /// carrier `T`, for a type no `T` describes (e.g. a reduced-precision
203    /// type for [`FilterPipeline::nbit`](crate::FilterPipeline::nbit)); an
204    /// element is `datatype.element_size()` bytes.
205    pub fn create_streaming_dataset_tiled_compressed_with_datatype(
206        &mut self,
207        name: &str,
208        datatype: crate::DatatypeMessage,
209        frame_dims: &[u64],
210        frame_chunk: &[u64],
211        pipeline: crate::format::messages::filter::FilterPipeline,
212    ) -> Result<usize> {
213        let idx = self.inner.create_streaming_dataset_tiled_compressed(
214            name,
215            datatype,
216            frame_dims,
217            frame_chunk,
218            pipeline,
219        )?;
220        Ok(idx)
221    }
222
223    /// Create a streaming dataset with full control over the chunk shape,
224    /// including the frame axis.
225    ///
226    /// `chunk` is the complete per-chunk shape, of rank
227    /// `frame_dims.len() + 1`: `chunk[0]` frames per chunk (the NDFileHDF5
228    /// `nFramesChunks` control) and `chunk[1..]` the per-frame tile shape
229    /// (`nRowChunks` / `nColChunks`). When `chunk[0] > 1`,
230    /// [`append_frame`](Self::append_frame) buffers whole frames until a
231    /// chunk band fills; the final partial band is written (zero-padded) at
232    /// [`close`](Self::close), and the dataset's logical frame count always
233    /// equals the exact number of frames appended.
234    pub fn create_streaming_dataset_chunked<T: H5Type>(
235        &mut self,
236        name: &str,
237        frame_dims: &[u64],
238        chunk: &[u64],
239    ) -> Result<usize> {
240        let idx =
241            self.inner
242                .create_streaming_dataset_chunked(name, T::hdf5_type(), frame_dims, chunk)?;
243        Ok(idx)
244    }
245
246    /// Compressed variant of
247    /// [`create_streaming_dataset_chunked`](Self::create_streaming_dataset_chunked);
248    /// each chunk is filtered independently through `pipeline`.
249    pub fn create_streaming_dataset_chunked_compressed<T: H5Type>(
250        &mut self,
251        name: &str,
252        frame_dims: &[u64],
253        chunk: &[u64],
254        pipeline: crate::format::messages::filter::FilterPipeline,
255    ) -> Result<usize> {
256        self.create_streaming_dataset_chunked_compressed_with_datatype(
257            name,
258            T::hdf5_type(),
259            frame_dims,
260            chunk,
261            pipeline,
262        )
263    }
264
265    /// [`Self::create_streaming_dataset_chunked_compressed`] with an explicit stored datatype instead of a
266    /// carrier `T`, for a type no `T` describes (e.g. a reduced-precision
267    /// type for [`FilterPipeline::nbit`](crate::FilterPipeline::nbit)); an
268    /// element is `datatype.element_size()` bytes.
269    pub fn create_streaming_dataset_chunked_compressed_with_datatype(
270        &mut self,
271        name: &str,
272        datatype: crate::DatatypeMessage,
273        frame_dims: &[u64],
274        chunk: &[u64],
275        pipeline: crate::format::messages::filter::FilterPipeline,
276    ) -> Result<usize> {
277        let idx = self.inner.create_streaming_dataset_chunked_compressed(
278            name, datatype, frame_dims, chunk, pipeline,
279        )?;
280        Ok(idx)
281    }
282
283    /// [`Self::create_streaming_dataset_chunked`] with a frame axis that holds
284    /// at most `max_frames` frames (`None`: unlimited) and an explicit stored
285    /// datatype, filtered through `pipeline` when one is given: NDFileHDF5's
286    /// maxdims\[0\] = NumCapture. A bounded frame axis is indexed by a fixed
287    /// array, the index libhdf5 picks for it, and an append past
288    /// `max_frames` is refused.
289    pub fn create_streaming_dataset_with_max_frames(
290        &mut self,
291        name: &str,
292        datatype: crate::DatatypeMessage,
293        frame_dims: &[u64],
294        chunk: &[u64],
295        max_frames: Option<u64>,
296        pipeline: Option<crate::format::messages::filter::FilterPipeline>,
297    ) -> Result<usize> {
298        let idx = self.inner.create_streaming_dataset_with_max_frames(
299            name, datatype, frame_dims, chunk, max_frames, pipeline,
300        )?;
301        Ok(idx)
302    }
303
304    /// Create a fixed-shape multi-dimensional grid dataset that fills at
305    /// explicit positions as frames arrive.
306    ///
307    /// Unlike [`create_streaming_dataset`](Self::create_streaming_dataset),
308    /// which appends frames along a single unlimited leading axis, this
309    /// creates a dataset of the full bounded shape `dims` (no unlimited axis)
310    /// and lets you place each frame at an arbitrary chunk position with
311    /// [`write_chunk_at`](Self::write_chunk_at). This mirrors AreaDetector's
312    /// "extra dimensions" layout, where a scan of known size (e.g.
313    /// `[Na, Nb, H, W]`) is filled in odometer order. `chunk` is the per-chunk
314    /// shape of the same rank (typically `[1, …, 1, H, W]`). `dims` is also
315    /// the maximum shape; [`set_extent`](Self::set_extent) moves the extent
316    /// within it. Returns the dataset index.
317    pub fn create_grid_dataset<T: H5Type>(
318        &mut self,
319        name: &str,
320        dims: &[u64],
321        chunk: &[u64],
322    ) -> Result<usize> {
323        self.create_grid_dataset_with_datatype(name, T::hdf5_type(), dims, chunk)
324    }
325
326    /// [`Self::create_grid_dataset`] with an explicit stored datatype instead of a
327    /// carrier `T`, for a type no `T` describes (e.g. a reduced-precision
328    /// type for [`FilterPipeline::nbit`](crate::FilterPipeline::nbit)); an
329    /// element is `datatype.element_size()` bytes.
330    pub fn create_grid_dataset_with_datatype(
331        &mut self,
332        name: &str,
333        datatype: crate::DatatypeMessage,
334        dims: &[u64],
335        chunk: &[u64],
336    ) -> Result<usize> {
337        let idx = self
338            .inner
339            .create_grid_dataset(name, datatype, dims, chunk)?;
340        Ok(idx)
341    }
342
343    /// Compressed variant of [`create_grid_dataset`](Self::create_grid_dataset);
344    /// each chunk is filtered independently through `pipeline`, and
345    /// [`write_chunk_raw_at`](Self::write_chunk_raw_at) can place chunks a
346    /// codec already compressed.
347    pub fn create_grid_dataset_compressed<T: H5Type>(
348        &mut self,
349        name: &str,
350        dims: &[u64],
351        chunk: &[u64],
352        pipeline: crate::format::messages::filter::FilterPipeline,
353    ) -> Result<usize> {
354        self.create_grid_dataset_compressed_with_datatype(
355            name,
356            T::hdf5_type(),
357            dims,
358            chunk,
359            pipeline,
360        )
361    }
362
363    /// [`Self::create_grid_dataset_compressed`] with an explicit stored
364    /// datatype instead of a carrier `T`, for a type no `T` describes (e.g. a
365    /// reduced-precision type for
366    /// [`FilterPipeline::nbit`](crate::FilterPipeline::nbit)); an element is
367    /// `datatype.element_size()` bytes.
368    pub fn create_grid_dataset_compressed_with_datatype(
369        &mut self,
370        name: &str,
371        datatype: crate::DatatypeMessage,
372        dims: &[u64],
373        chunk: &[u64],
374        pipeline: crate::format::messages::filter::FilterPipeline,
375    ) -> Result<usize> {
376        let idx = self
377            .inner
378            .create_grid_dataset_compressed(name, datatype, dims, chunk, pipeline)?;
379        Ok(idx)
380    }
381
382    /// Create a hard link: an additional name for a dataset or group that
383    /// already exists in the file.
384    ///
385    /// No data is copied — the link and its target share one object header,
386    /// exactly as `h5py` / libhdf5 hard links do. This is the NeXus-style way
387    /// to expose a streaming dataset at an aliased path.
388    ///
389    /// * `parent_group_path` — full path of the group that will hold the
390    ///   link (`"/"` for the root group).
391    /// * `link_name` — leaf name of the new link within that group.
392    /// * `target_path` — full path of an existing dataset or group.
393    ///
394    /// # Visibility relative to SWMR mode
395    ///
396    /// A link created **before** [`start_swmr`](Self::start_swmr) is committed
397    /// by `start_swmr` and is visible to SWMR readers for the whole streaming
398    /// window. A link created **after** `start_swmr` is committed only by
399    /// [`close`](Self::close); it does not appear to readers that attach
400    /// during the live SWMR window. Create layout links before `start_swmr`
401    /// when readers must resolve them while streaming.
402    pub fn create_hard_link(
403        &mut self,
404        parent_group_path: &str,
405        link_name: &str,
406        target_path: &str,
407    ) -> Result<()> {
408        self.inner
409            .writer_mut()
410            .create_hard_link(parent_group_path, link_name, target_path)?;
411        Ok(())
412    }
413
414    /// Create a group in the file hierarchy.
415    ///
416    /// * `parent_group_path` — full path of the parent group (`"/"` for the
417    ///   root group).
418    /// * `name` — leaf name of the new group.
419    ///
420    /// A nested NeXus layout is built one level at a time, parent first:
421    ///
422    /// ```no_run
423    /// # use rust_hdf5::swmr::SwmrFileWriter;
424    /// # let mut writer = SwmrFileWriter::create("stream.h5").unwrap();
425    /// writer.create_group("/", "entry").unwrap();
426    /// writer.create_group("/entry", "data").unwrap();
427    /// ```
428    ///
429    /// Like [`create_hard_link`](Self::create_hard_link), a group created
430    /// before [`start_swmr`](Self::start_swmr) is visible to SWMR readers for
431    /// the whole streaming window; one created after is committed only by
432    /// [`close`](Self::close).
433    pub fn create_group(&mut self, parent_group_path: &str, name: &str) -> Result<()> {
434        self.inner
435            .writer_mut()
436            .create_group(parent_group_path, name)?;
437        Ok(())
438    }
439
440    /// Set a string attribute on a group, or on the root group when
441    /// `group_path` is `"/"`.
442    ///
443    /// This is the NeXus way to tag a group with its class — for example
444    /// `set_group_attr_string("/entry", "NX_class", "NXentry")`. An existing
445    /// attribute of the same name is replaced.
446    ///
447    /// After [`start_swmr`](Self::start_swmr) a set or replace is committed
448    /// at close, as libhdf5 1.14 commits it under SWMR; until then readers
449    /// see the attributes as published. This holds for every attribute
450    /// setter.
451    pub fn set_group_attr_string(
452        &mut self,
453        group_path: &str,
454        name: &str,
455        value: &str,
456    ) -> Result<()> {
457        self.inner.writer_mut().set_vlen_string_attribute(
458            group_attr_target(group_path),
459            name,
460            value,
461        )?;
462        Ok(())
463    }
464
465    /// Set a numeric scalar attribute on a group, or on the root group when
466    /// `group_path` is `"/"`. An existing attribute of the same name is
467    /// replaced. After [`start_swmr`](Self::start_swmr) it is committed at
468    /// close — see [`set_group_attr_string`](Self::set_group_attr_string).
469    pub fn set_group_attr_numeric<T: H5Type>(
470        &mut self,
471        group_path: &str,
472        name: &str,
473        value: &T,
474    ) -> Result<()> {
475        let attr = AttributeMessage::scalar_numeric(name, T::hdf5_type(), scalar_to_bytes(value));
476        self.inner
477            .writer_mut()
478            .set_attribute(group_attr_target(group_path), attr)?;
479        Ok(())
480    }
481
482    /// Set a numeric array attribute on a group, or on the root group when
483    /// `group_path` is `"/"` — the group form of
484    /// [`set_dataset_attr_array`](Self::set_dataset_attr_array), with the
485    /// same shape rule. After [`start_swmr`](Self::start_swmr) it is
486    /// committed at close — see
487    /// [`set_group_attr_string`](Self::set_group_attr_string).
488    pub fn set_group_attr_array<T: H5Type>(
489        &mut self,
490        group_path: &str,
491        name: &str,
492        dims: &[u64],
493        values: &[T],
494    ) -> Result<()> {
495        let attr = array_attr(name, dims, values)?;
496        self.inner
497            .writer_mut()
498            .set_attribute(group_attr_target(group_path), attr)?;
499        Ok(())
500    }
501
502    /// Set a scalar attribute of a caller-supplied datatype on a group, or on
503    /// the root group when `group_path` is `"/"` — the SWMR form of
504    /// [`H5Group::set_attr_typed`](crate::H5Group::set_attr_typed). After
505    /// [`start_swmr`](Self::start_swmr) it is committed at close — see
506    /// [`set_group_attr_string`](Self::set_group_attr_string).
507    pub fn set_group_attr_typed(
508        &mut self,
509        group_path: &str,
510        name: &str,
511        datatype: DatatypeMessage,
512        value: Vec<u8>,
513    ) -> Result<()> {
514        let attr = crate::attribute::typed_scalar_attr(name, datatype, value)?;
515        self.inner
516            .writer_mut()
517            .set_attribute(group_attr_target(group_path), attr)?;
518        Ok(())
519    }
520
521    /// Create a fixed-shape (non-streaming) dataset and write all its data in
522    /// one call. Returns the dataset index.
523    ///
524    /// This is for the NeXus metadata that surrounds the image stream —
525    /// coordinate axes, detector geometry, and (with `dims = &[]`) scalar
526    /// values such as `/entry/instrument/detector/distance`. Unlike a
527    /// streaming dataset, it is written once and not appended to.
528    pub fn write_dataset<T: H5Type>(
529        &mut self,
530        name: &str,
531        dims: &[u64],
532        data: &[T],
533    ) -> Result<usize> {
534        let expected: u64 = if dims.is_empty() {
535            1
536        } else {
537            dims.iter().product()
538        };
539        if data.len() as u64 != expected {
540            return Err(crate::error::Hdf5Error::InvalidState(format!(
541                "write_dataset: data has {} elements but shape {dims:?} needs {expected}",
542                data.len()
543            )));
544        }
545        let idx = self
546            .inner
547            .writer_mut()
548            .create_dataset(name, T::hdf5_type(), dims)?;
549        self.inner
550            .writer_mut()
551            .write_dataset_raw(idx, slice_to_bytes(data))?;
552        Ok(idx)
553    }
554
555    /// [`write_dataset`](Self::write_dataset) with a caller-supplied datatype
556    /// and the raw element bytes, `dims` product times the type's size — for
557    /// NDFileHDF5's NUL-terminated fixed-length string constants, where
558    /// [`write_string_dataset`](Self::write_string_dataset) writes a
559    /// variable-length one. A byte count that does not match is refused
560    /// before the dataset is created.
561    pub fn write_dataset_typed(
562        &mut self,
563        name: &str,
564        datatype: DatatypeMessage,
565        dims: &[u64],
566        data: &[u8],
567    ) -> Result<usize> {
568        let expected = dims.iter().product::<u64>() * datatype.element_size() as u64;
569        if data.len() as u64 != expected {
570            return Err(Hdf5Error::InvalidState(format!(
571                "write_dataset_typed: data has {} bytes but shape {dims:?} needs {expected}",
572                data.len()
573            )));
574        }
575        let idx = self
576            .inner
577            .writer_mut()
578            .create_dataset(name, datatype, dims)?;
579        self.inner.writer_mut().write_dataset_raw(idx, data)?;
580        Ok(idx)
581    }
582
583    /// Create a variable-length string dataset (one element per string).
584    /// Returns the dataset index. Useful for NeXus metadata such as
585    /// `/entry/start_time` or per-frame timestamp arrays.
586    ///
587    /// The datatype declares UTF-8;
588    /// [`write_string_dataset_ascii`](Self::write_string_dataset_ascii)
589    /// declares ASCII instead.
590    pub fn write_string_dataset(&mut self, name: &str, strings: &[&str]) -> Result<usize> {
591        let idx = self
592            .inner
593            .writer_mut()
594            .create_vlen_string_dataset(name, strings, 1)?;
595        Ok(idx)
596    }
597
598    /// [`write_string_dataset`](Self::write_string_dataset) under an **ASCII**
599    /// datatype, the one h5py's `string_dtype("ascii")` produces. A string
600    /// that is not 7-bit is rejected rather than mislabelled.
601    pub fn write_string_dataset_ascii(&mut self, name: &str, strings: &[&str]) -> Result<usize> {
602        let idx = self
603            .inner
604            .writer_mut()
605            .create_vlen_string_dataset(name, strings, 0)?;
606        Ok(idx)
607    }
608
609    /// Set a string attribute on a dataset, addressed by its index. The
610    /// NeXus way to record `units`, `long_name`, `signal`, etc. An existing
611    /// attribute of the same name is replaced. After
612    /// [`start_swmr`](Self::start_swmr) it is committed at close — see
613    /// [`set_group_attr_string`](Self::set_group_attr_string).
614    pub fn set_dataset_attr_string(
615        &mut self,
616        ds_index: usize,
617        name: &str,
618        value: &str,
619    ) -> Result<()> {
620        self.inner.writer_mut().set_vlen_string_attribute(
621            crate::io::writer::AttrTarget::Dataset(ds_index),
622            name,
623            value,
624        )?;
625        Ok(())
626    }
627
628    /// Set a numeric scalar attribute on a dataset, addressed by its index.
629    /// An existing attribute of the same name is replaced. After
630    /// [`start_swmr`](Self::start_swmr) it is committed at close — see
631    /// [`set_group_attr_string`](Self::set_group_attr_string).
632    pub fn set_dataset_attr_numeric<T: H5Type>(
633        &mut self,
634        ds_index: usize,
635        name: &str,
636        value: &T,
637    ) -> Result<()> {
638        let attr = AttributeMessage::scalar_numeric(name, T::hdf5_type(), scalar_to_bytes(value));
639        self.inner
640            .writer_mut()
641            .add_dataset_attribute(ds_index, attr)?;
642        Ok(())
643    }
644
645    /// Set a scalar attribute of a caller-supplied datatype on a dataset,
646    /// addressed by its index — the SWMR form of
647    /// [`H5Dataset::set_attr_typed`](crate::H5Dataset::set_attr_typed).
648    /// After [`start_swmr`](Self::start_swmr) it is committed at close — see
649    /// [`set_group_attr_string`](Self::set_group_attr_string).
650    pub fn set_dataset_attr_typed(
651        &mut self,
652        ds_index: usize,
653        name: &str,
654        datatype: DatatypeMessage,
655        value: Vec<u8>,
656    ) -> Result<()> {
657        let attr = crate::attribute::typed_scalar_attr(name, datatype, value)?;
658        self.inner
659            .writer_mut()
660            .add_dataset_attribute(ds_index, attr)?;
661        Ok(())
662    }
663
664    /// Set a numeric array attribute on a dataset, addressed by its index.
665    ///
666    /// `dims` are the dimension sizes (e.g. `&[3]` for a 1-D array) and the
667    /// number of `values` must equal their product. This is the SWMR
668    /// counterpart of [`H5Attribute::write_array`](crate::H5Attribute::write_array),
669    /// for the length-ndims `int32` array attributes AreaDetector writes
670    /// (`NDArrayDimOffset`, `NDArrayDimBinning`, `NDArrayDimReverse`). An
671    /// existing attribute of the same name is replaced.
672    ///
673    /// After [`start_swmr`](Self::start_swmr) it is committed at close — see
674    /// [`set_group_attr_string`](Self::set_group_attr_string). Resolve a
675    /// dataset path to its index with [`dataset_index`](Self::dataset_index).
676    pub fn set_dataset_attr_array<T: H5Type>(
677        &mut self,
678        ds_index: usize,
679        name: &str,
680        dims: &[u64],
681        values: &[T],
682    ) -> Result<()> {
683        let attr = array_attr(name, dims, values)?;
684        self.inner
685            .writer_mut()
686            .add_dataset_attribute(ds_index, attr)?;
687        Ok(())
688    }
689
690    /// Set the fill value of a streaming dataset, addressed by its index.
691    ///
692    /// Call this before the first [`append_frame`](Self::append_frame): it
693    /// determines the value of chunk regions that are never written (a
694    /// partial final band, or unwritten tiles).
695    pub fn set_dataset_fill_value<T: H5Type>(&mut self, ds_index: usize, value: &T) -> Result<()> {
696        self.set_dataset_fill_value_bytes(ds_index, &scalar_to_bytes(value))
697    }
698
699    /// [`set_dataset_fill_value`](Self::set_dataset_fill_value) from one
700    /// element encoded in the dataset's stored datatype — for a type no `T`
701    /// carries, such as one made with a `_with_datatype` constructor.
702    /// `bytes` is the element's host-order image, `datatype.element_size()`
703    /// bytes long; it is put in the order the datatype declares.
704    pub fn set_dataset_fill_value_bytes(&mut self, ds_index: usize, bytes: &[u8]) -> Result<()> {
705        let writer = self.inner.writer_mut();
706        if ds_index >= writer.dataset_count() {
707            return Err(Hdf5Error::InvalidState(format!(
708                "dataset index {ds_index} out of range"
709            )));
710        }
711        let datatype = writer.ds(ds_index).lock().datatype.clone();
712        let width = datatype.element_size() as usize;
713        if bytes.len() != width {
714            return Err(Hdf5Error::TypeMismatch(format!(
715                "fill value is {} bytes but datatype {datatype} stores {width}",
716                bytes.len()
717            )));
718        }
719        let stored = crate::dataset::to_stored_byte_order(bytes, &datatype, width)?;
720        writer.set_dataset_fill_value(ds_index, stored.into_owned())?;
721        Ok(())
722    }
723
724    /// Place an existing dataset inside a group.
725    ///
726    /// By default a dataset created through this writer lives at the root
727    /// level; this moves its link record into `group_path` (which must
728    /// already exist). The group must be created before `start_swmr` for the
729    /// placement to be visible to readers during streaming.
730    pub fn assign_dataset_to_group(&mut self, group_path: &str, ds_index: usize) -> Result<()> {
731        self.inner
732            .writer_mut()
733            .assign_dataset_to_group(group_path, ds_index)?;
734        Ok(())
735    }
736
737    /// Signal the start of SWMR mode.
738    pub fn start_swmr(&mut self) -> Result<()> {
739        self.inner.start_swmr()?;
740        Ok(())
741    }
742
743    /// Append a frame of raw data to a streaming dataset.
744    ///
745    /// The data size must match one frame (product of frame_dims * element_size).
746    pub fn append_frame(&mut self, ds_index: usize, data: &[u8]) -> Result<()> {
747        self.inner.append_frame(ds_index, data)?;
748        Ok(())
749    }
750
751    /// Append a frame that is already filtered (e.g. compressed by a codec
752    /// plugin upstream), storing its bytes verbatim — HDF5's direct chunk
753    /// write (`H5Dwrite_chunk`) for a streaming dataset.
754    ///
755    /// `data` is the stored (compressed) image of one frame; the dataset's
756    /// filter pipeline, recorded when it was created with a `_compressed`
757    /// constructor, is what readers run to decode it. `filter_mask` bit *i*
758    /// set means filter *i* was not applied to this frame; pass 0 when the
759    /// whole pipeline was. The dataset must store one whole frame per chunk
760    /// ([`create_streaming_dataset_compressed`](Self::create_streaming_dataset_compressed),
761    /// or a `_chunked_compressed` or filtered `_with_max_frames` chunk of
762    /// `[1, frame_dims...]`).
763    pub fn append_frame_raw(
764        &mut self,
765        ds_index: usize,
766        data: &[u8],
767        filter_mask: u32,
768    ) -> Result<()> {
769        self.inner.append_frame_raw(ds_index, data, filter_mask)?;
770        Ok(())
771    }
772
773    /// Write one already-filtered chunk verbatim at an explicit chunk
774    /// position of a grid dataset made by
775    /// [`create_grid_dataset_compressed`](Self::create_grid_dataset_compressed)
776    /// — HDF5's direct chunk write (`H5Dwrite_chunk`) at a chunk offset.
777    /// `filter_mask` bit *i* set means filter *i* was not applied to this
778    /// chunk; pass 0 when the whole pipeline was.
779    pub fn write_chunk_raw_at(
780        &mut self,
781        ds_index: usize,
782        chunk_coords: &[u64],
783        data: &[u8],
784        filter_mask: u32,
785    ) -> Result<()> {
786        self.inner
787            .write_chunk_raw_at(ds_index, chunk_coords, data, filter_mask)?;
788        Ok(())
789    }
790
791    /// Write one frame at an explicit chunk position of a grid dataset
792    /// created with [`create_grid_dataset`](Self::create_grid_dataset).
793    ///
794    /// `chunk_coords` are in units of chunks (row-major over the chunk grid)
795    /// and `data` must be exactly one full chunk (`product(chunk) *
796    /// element_size` bytes; edge chunks are zero-padded by the caller).
797    /// Positions may be written in any order, beyond the extent too while
798    /// inside the maximum shape, and unwritten positions read back as fill.
799    /// As with the streaming path, call [`flush`](Self::flush) to make writes
800    /// visible to SWMR readers and set dataset attributes before
801    /// [`start_swmr`](Self::start_swmr).
802    pub fn write_chunk_at(
803        &mut self,
804        ds_index: usize,
805        chunk_coords: &[u64],
806        data: &[u8],
807    ) -> Result<()> {
808        self.inner.write_chunk_at(ds_index, chunk_coords, data)?;
809        Ok(())
810    }
811
812    /// Set a dataset's logical extent within its maximum shape, growing or
813    /// shrinking it — libhdf5's `H5Dset_extent`, e.g. a grid's extent
814    /// following the positions written so far. A multi-frame-chunk stream,
815    /// whose extent counts the frames it buffers, is refused.
816    pub fn set_extent(&mut self, ds_index: usize, dims: &[u64]) -> Result<()> {
817        self.inner.set_extent(ds_index, dims)?;
818        Ok(())
819    }
820
821    /// Flush all dataset index structures to disk with SWMR ordering.
822    pub fn flush(&mut self) -> Result<()> {
823        self.inner.flush()?;
824        Ok(())
825    }
826
827    /// Close and finalize the file.
828    pub fn close(self) -> Result<()> {
829        self.inner.close()?;
830        Ok(())
831    }
832}
833
834/// SWMR reader for monitoring a streaming HDF5 file.
835///
836/// Opens a file being written by a concurrent [`SwmrFileWriter`] and
837/// periodically calls [`refresh`](Self::refresh) to pick up new data.
838///
839/// ```no_run
840/// use rust_hdf5::swmr::SwmrFileReader;
841///
842/// let mut reader = SwmrFileReader::open("stream.h5").unwrap();
843///
844/// loop {
845///     reader.refresh().unwrap();
846///     let names = reader.dataset_names();
847///     if let Some(shape) = reader.dataset_shape("frames").ok() {
848///         println!("frames shape: {:?}", shape);
849///         if shape[0] > 0 {
850///             let data = reader.read_dataset_raw("frames").unwrap();
851///             println!("got {} bytes", data.len());
852///             break;
853///         }
854///     }
855///     std::thread::sleep(std::time::Duration::from_millis(100));
856/// }
857/// ```
858pub struct SwmrFileReader {
859    reader: Hdf5Reader,
860}
861
862impl SwmrFileReader {
863    /// Open an HDF5 file for SWMR reading using the env-var-derived
864    /// locking policy. Takes a shared lock so it coexists with the
865    /// downgraded shared lock held by [`SwmrFileWriter`] after
866    /// `start_swmr`, and with other concurrent SWMR readers.
867    pub fn open<P: AsRef<Path>>(path: P) -> Result<Self> {
868        let reader = Hdf5Reader::open_swmr(path.as_ref())?;
869        Ok(Self { reader })
870    }
871
872    /// Open an HDF5 file for SWMR reading with an explicit locking policy.
873    pub fn open_with_locking<P: AsRef<Path>>(path: P, locking: FileLocking) -> Result<Self> {
874        let reader = Hdf5Reader::open_swmr_with_locking(path.as_ref(), locking)?;
875        Ok(Self { reader })
876    }
877
878    /// Re-read the superblock and dataset metadata from disk.
879    ///
880    /// Call this periodically to pick up new data written by the concurrent
881    /// SWMR writer.
882    pub fn refresh(&mut self) -> Result<()> {
883        self.reader.refresh()?;
884        Ok(())
885    }
886
887    /// Return the names of all datasets.
888    pub fn dataset_names(&self) -> Vec<String> {
889        self.reader
890            .dataset_names()
891            .iter()
892            .map(|s| s.to_string())
893            .collect()
894    }
895
896    /// Return the current shape of a dataset.
897    pub fn dataset_shape(&mut self, name: &str) -> Result<Vec<u64>> {
898        Ok(self.reader.dataset_shape(name)?)
899    }
900
901    /// Read the raw bytes of a dataset.
902    pub fn read_dataset_raw(&mut self, name: &str) -> Result<Vec<u8>> {
903        Ok(self.reader.read_dataset_raw(name)?)
904    }
905
906    /// Read a dataset as a typed vector.
907    ///
908    /// `T` must have the dataset's exact element width — the same
909    /// width-checked byte reinterpretation as
910    /// [`H5Dataset::read_raw`](crate::dataset::H5Dataset::read_raw), with no
911    /// datatype conversion. Use
912    /// [`dataset_element_size`](Self::dataset_element_size) to size-check a
913    /// type at runtime.
914    pub fn read_dataset<T: H5Type>(&mut self, name: &str) -> Result<Vec<T>> {
915        self.check_element_width::<T>(name)?;
916        let datatype = self.dataset_datatype(name)?;
917        let es = T::element_size();
918        let total = self.reader.dataset_raw_size(name)? as usize;
919        if es == 0 || !total.is_multiple_of(es) {
920            return Err(crate::error::Hdf5Error::TypeMismatch(format!(
921                "raw data size {total} is not a multiple of element size {es}"
922            )));
923        }
924        // As the non-SWMR full read: the image lands in the vector this
925        // returns rather than in a byte buffer copied into it afterwards.
926        crate::io::reader::read_image_into_new(total / es, |image| {
927            self.reader.read_dataset_raw_into_dst(
928                name,
929                image,
930                crate::io::file_handle::ReadDst::Fresh,
931            )?;
932            crate::dataset::to_host_byte_order(image, &datatype, es)
933        })
934    }
935
936    /// Read a slice (hyperslab) of a dataset as raw bytes.
937    ///
938    /// `starts[d]` is the first index along dimension `d`, `counts[d]` is how
939    /// many. For a streaming dataset this reads only the chunks the slice
940    /// overlaps — the efficient way for a live viewer to fetch the latest
941    /// frame without re-reading the whole stream.
942    pub fn read_slice_raw(
943        &mut self,
944        name: &str,
945        starts: &[u64],
946        counts: &[u64],
947    ) -> Result<Vec<u8>> {
948        Ok(self.reader.read_slice(name, starts, counts)?)
949    }
950
951    /// Read a slice (hyperslab) of a dataset as a typed vector.
952    /// See [`read_slice_raw`](Self::read_slice_raw); `T`'s width is checked
953    /// like [`read_dataset`](Self::read_dataset).
954    pub fn read_slice<T: H5Type>(
955        &mut self,
956        name: &str,
957        starts: &[u64],
958        counts: &[u64],
959    ) -> Result<Vec<T>> {
960        self.check_element_width::<T>(name)?;
961        let datatype = self.dataset_datatype(name)?;
962        bytes_to_typed(self.reader.read_slice(name, starts, counts)?, &datatype)
963    }
964
965    /// The width gate for the typed reads: without it, reinterpreting the
966    /// raw bytes splits or merges elements — an f64 dataset read as `i32`
967    /// passed the divisibility check and silently returned twice as many
968    /// garbage values.
969    fn check_element_width<T: H5Type>(&mut self, name: &str) -> Result<()> {
970        let stored = self.dataset_element_size(name)?;
971        if T::element_size() != stored {
972            return Err(crate::error::Hdf5Error::TypeMismatch(format!(
973                "read type has element size {} but dataset has element size {stored}",
974                T::element_size(),
975            )));
976        }
977        Ok(())
978    }
979
980    /// Read a variable-length string dataset.
981    pub fn read_vlen_strings(&mut self, name: &str) -> Result<Vec<String>> {
982        Ok(self.reader.read_vlen_strings(name)?)
983    }
984
985    /// Element size of a dataset's datatype, in bytes — enough to size a
986    /// read buffer without knowing the concrete element type at compile time.
987    pub fn dataset_element_size(&mut self, name: &str) -> Result<usize> {
988        self.reader
989            .dataset_info(name)
990            .map(|i| i.datatype.element_size() as usize)
991            .ok_or_else(|| crate::error::Hdf5Error::NotFound(name.to_string()))
992    }
993
994    /// The stored datatype of a dataset, which the typed reads need to put
995    /// the element image into host byte order.
996    fn dataset_datatype(&mut self, name: &str) -> Result<DatatypeMessage> {
997        self.reader
998            .dataset_info(name)
999            .map(|i| i.datatype.clone())
1000            .ok_or_else(|| crate::error::Hdf5Error::NotFound(name.to_string()))
1001    }
1002
1003    /// All group paths in the file.
1004    pub fn group_paths(&self) -> Vec<String> {
1005        self.reader.group_paths().iter().cloned().collect()
1006    }
1007
1008    /// Whether a group exists. A leading `/` is tolerated.
1009    pub fn has_group(&self, group_path: &str) -> bool {
1010        self.reader.has_group(group_path.trim_start_matches('/'))
1011    }
1012
1013    /// Names of the attributes on a dataset.
1014    pub fn dataset_attr_names(&mut self, name: &str) -> Result<Vec<String>> {
1015        Ok(self.reader.dataset_attr_names(name)?)
1016    }
1017
1018    /// Read a dataset's attribute as a string (e.g. `units`, `NX_class`).
1019    pub fn dataset_attr_string(&mut self, dataset: &str, attr: &str) -> Result<String> {
1020        let a = self.reader.dataset_attr(dataset, attr)?.clone();
1021        Ok(self.reader.attr_string_value(&a)?)
1022    }
1023
1024    /// Names of the attributes on a group, or on the root group when
1025    /// `group_path` is `"/"`. A leading `/` is tolerated.
1026    pub fn group_attr_names(&mut self, group_path: &str) -> Result<Vec<String>> {
1027        Ok(if group_path == "/" {
1028            self.reader.root_attr_names()?
1029        } else {
1030            self.reader
1031                .group_attr_names(group_path.trim_start_matches('/'))?
1032        })
1033    }
1034
1035    /// Read a group's attribute as a string (the NeXus `NX_class` etc.), or
1036    /// a root attribute when `group_path` is `"/"`. A leading `/` is tolerated.
1037    pub fn group_attr_string(&mut self, group_path: &str, attr: &str) -> Result<String> {
1038        let a = if group_path == "/" {
1039            self.reader.root_attr(attr)
1040        } else {
1041            self.reader
1042                .group_attr(group_path.trim_start_matches('/'), attr)
1043        }?
1044        .clone();
1045        Ok(self.reader.attr_string_value(&a)?)
1046    }
1047}
1048
1049/// The writer-side attribute list a group path names: `"/"` is the
1050/// file-level (root) list, anything else the group's own.
1051fn group_attr_target(group_path: &str) -> crate::io::writer::AttrTarget<'_> {
1052    if group_path == "/" {
1053        crate::io::writer::AttrTarget::Root
1054    } else {
1055        crate::io::writer::AttrTarget::Group(group_path)
1056    }
1057}
1058
1059/// Reinterpret a raw byte buffer as a typed vector. The buffer length must
1060/// be a whole multiple of `T`'s element size, and the stored byte order is
1061/// converted to the host's first — reinterpretation is only the stored value
1062/// when the two agree.
1063fn bytes_to_typed<T: H5Type>(mut raw: Vec<u8>, datatype: &DatatypeMessage) -> Result<Vec<T>> {
1064    let es = T::element_size();
1065    if es == 0 || !raw.len().is_multiple_of(es) {
1066        return Err(crate::error::Hdf5Error::TypeMismatch(format!(
1067            "raw data size {} is not a multiple of element size {es}",
1068            raw.len()
1069        )));
1070    }
1071    crate::dataset::to_host_byte_order(&mut raw, datatype, es)?;
1072    let count = raw.len() / es;
1073    let mut result = Vec::<T>::with_capacity(count);
1074    // Safety: `T: H5Type` is a `Copy` POD primitive exactly `element_size()`
1075    // bytes wide, so the byte buffer is a valid array of `count` `T`s.
1076    unsafe {
1077        std::ptr::copy_nonoverlapping(raw.as_ptr(), result.as_mut_ptr() as *mut u8, raw.len());
1078        result.set_len(count);
1079    }
1080    Ok(result)
1081}
1082
1083/// The array attribute message for `values` of shape `dims`, whose product
1084/// must be the value count (an empty shape is a scalar, one value).
1085fn array_attr<T: H5Type>(name: &str, dims: &[u64], values: &[T]) -> Result<AttributeMessage> {
1086    let expected: u64 = dims.iter().product();
1087    if values.len() as u64 != expected {
1088        return Err(crate::error::Hdf5Error::InvalidState(format!(
1089            "attribute '{name}': {} values but shape {dims:?} needs {expected}",
1090            values.len()
1091        )));
1092    }
1093    Ok(AttributeMessage::array_numeric(
1094        name,
1095        T::hdf5_type(),
1096        dims,
1097        slice_to_bytes(values).to_vec(),
1098    ))
1099}
1100
1101/// Raw bytes of one `H5Type` scalar.
1102fn scalar_to_bytes<T: H5Type>(value: &T) -> Vec<u8> {
1103    let es = T::element_size();
1104    // Safety: `T: H5Type` is a `Copy` POD primitive exactly `element_size()`
1105    // bytes wide.
1106    unsafe { std::slice::from_raw_parts(value as *const T as *const u8, es) }.to_vec()
1107}
1108
1109/// Raw bytes of an `H5Type` slice, in element order.
1110fn slice_to_bytes<T: H5Type>(data: &[T]) -> &[u8] {
1111    // Safety: as `scalar_to_bytes`; the slice is a contiguous POD array.
1112    unsafe { std::slice::from_raw_parts(data.as_ptr() as *const u8, std::mem::size_of_val(data)) }
1113}