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    /// [`Self::create_grid_dataset_with_datatype`] at extent `dims` within
383    /// `max_dims` (`None`: unlimited), filtered through `pipeline` when one
384    /// is given. One unlimited dimension takes an extensible-array chunk
385    /// index and more take a v2 B-tree, as libhdf5 picks them; positions are
386    /// written with [`write_chunk_at`](Self::write_chunk_at) as on a fixed
387    /// grid, and [`set_extent`](Self::set_extent) moves the extent.
388    pub fn create_grid_dataset_with_max(
389        &mut self,
390        name: &str,
391        datatype: crate::DatatypeMessage,
392        dims: &[u64],
393        max_dims: &[Option<u64>],
394        chunk: &[u64],
395        pipeline: Option<crate::format::messages::filter::FilterPipeline>,
396    ) -> Result<usize> {
397        let max: Vec<u64> = max_dims.iter().map(|m| m.unwrap_or(u64::MAX)).collect();
398        let idx = self
399            .inner
400            .create_grid_dataset_with_max(name, datatype, dims, &max, chunk, pipeline)?;
401        Ok(idx)
402    }
403
404    /// Create a hard link: an additional name for a dataset or group that
405    /// already exists in the file.
406    ///
407    /// No data is copied — the link and its target share one object header,
408    /// exactly as `h5py` / libhdf5 hard links do. This is the NeXus-style way
409    /// to expose a streaming dataset at an aliased path.
410    ///
411    /// * `parent_group_path` — full path of the group that will hold the
412    ///   link (`"/"` for the root group).
413    /// * `link_name` — leaf name of the new link within that group.
414    /// * `target_path` — full path of an existing dataset or group.
415    ///
416    /// # Visibility relative to SWMR mode
417    ///
418    /// A link created **before** [`start_swmr`](Self::start_swmr) is committed
419    /// by `start_swmr` and is visible to SWMR readers for the whole streaming
420    /// window. A link created **after** `start_swmr` is committed only by
421    /// [`close`](Self::close); it does not appear to readers that attach
422    /// during the live SWMR window. Create layout links before `start_swmr`
423    /// when readers must resolve them while streaming.
424    pub fn create_hard_link(
425        &mut self,
426        parent_group_path: &str,
427        link_name: &str,
428        target_path: &str,
429    ) -> Result<()> {
430        self.inner
431            .writer_mut()
432            .create_hard_link(parent_group_path, link_name, target_path)?;
433        Ok(())
434    }
435
436    /// Create a group in the file hierarchy.
437    ///
438    /// * `parent_group_path` — full path of the parent group (`"/"` for the
439    ///   root group).
440    /// * `name` — leaf name of the new group.
441    ///
442    /// A nested NeXus layout is built one level at a time, parent first:
443    ///
444    /// ```no_run
445    /// # use rust_hdf5::swmr::SwmrFileWriter;
446    /// # let mut writer = SwmrFileWriter::create("stream.h5").unwrap();
447    /// writer.create_group("/", "entry").unwrap();
448    /// writer.create_group("/entry", "data").unwrap();
449    /// ```
450    ///
451    /// Like [`create_hard_link`](Self::create_hard_link), a group created
452    /// before [`start_swmr`](Self::start_swmr) is visible to SWMR readers for
453    /// the whole streaming window; one created after is committed only by
454    /// [`close`](Self::close).
455    pub fn create_group(&mut self, parent_group_path: &str, name: &str) -> Result<()> {
456        self.inner
457            .writer_mut()
458            .create_group(parent_group_path, name)?;
459        Ok(())
460    }
461
462    /// Set a string attribute on a group, or on the root group when
463    /// `group_path` is `"/"`.
464    ///
465    /// This is the NeXus way to tag a group with its class — for example
466    /// `set_group_attr_string("/entry", "NX_class", "NXentry")`. An existing
467    /// attribute of the same name is replaced.
468    ///
469    /// After [`start_swmr`](Self::start_swmr) a set or replace is committed
470    /// at close, as libhdf5 1.14 commits it under SWMR; until then readers
471    /// see the attributes as published. This holds for every attribute
472    /// setter.
473    pub fn set_group_attr_string(
474        &mut self,
475        group_path: &str,
476        name: &str,
477        value: &str,
478    ) -> Result<()> {
479        self.inner.writer_mut().set_vlen_string_attribute(
480            group_attr_target(group_path),
481            name,
482            value,
483        )?;
484        Ok(())
485    }
486
487    /// Set a numeric scalar attribute on a group, or on the root group when
488    /// `group_path` is `"/"`. An existing attribute of the same name is
489    /// replaced. After [`start_swmr`](Self::start_swmr) it is committed at
490    /// close — see [`set_group_attr_string`](Self::set_group_attr_string).
491    pub fn set_group_attr_numeric<T: H5Type>(
492        &mut self,
493        group_path: &str,
494        name: &str,
495        value: &T,
496    ) -> Result<()> {
497        let attr = AttributeMessage::scalar_numeric(name, T::hdf5_type(), scalar_to_bytes(value));
498        self.inner
499            .writer_mut()
500            .set_attribute(group_attr_target(group_path), attr)?;
501        Ok(())
502    }
503
504    /// Set a numeric array attribute on a group, or on the root group when
505    /// `group_path` is `"/"` — the group form of
506    /// [`set_dataset_attr_array`](Self::set_dataset_attr_array), with the
507    /// same shape rule. After [`start_swmr`](Self::start_swmr) it is
508    /// committed at close — see
509    /// [`set_group_attr_string`](Self::set_group_attr_string).
510    pub fn set_group_attr_array<T: H5Type>(
511        &mut self,
512        group_path: &str,
513        name: &str,
514        dims: &[u64],
515        values: &[T],
516    ) -> Result<()> {
517        let attr = array_attr(name, dims, values)?;
518        self.inner
519            .writer_mut()
520            .set_attribute(group_attr_target(group_path), attr)?;
521        Ok(())
522    }
523
524    /// Set a scalar attribute of a caller-supplied datatype on a group, or on
525    /// the root group when `group_path` is `"/"` — the SWMR form of
526    /// [`H5Group::set_attr_typed`](crate::H5Group::set_attr_typed). After
527    /// [`start_swmr`](Self::start_swmr) it is committed at close — see
528    /// [`set_group_attr_string`](Self::set_group_attr_string).
529    pub fn set_group_attr_typed(
530        &mut self,
531        group_path: &str,
532        name: &str,
533        datatype: DatatypeMessage,
534        value: Vec<u8>,
535    ) -> Result<()> {
536        let attr = crate::attribute::typed_scalar_attr(name, datatype, value)?;
537        self.inner
538            .writer_mut()
539            .set_attribute(group_attr_target(group_path), attr)?;
540        Ok(())
541    }
542
543    /// Create a fixed-shape (non-streaming) dataset and write all its data in
544    /// one call. Returns the dataset index.
545    ///
546    /// This is for the NeXus metadata that surrounds the image stream —
547    /// coordinate axes, detector geometry, and (with `dims = &[]`) scalar
548    /// values such as `/entry/instrument/detector/distance`. Unlike a
549    /// streaming dataset, it is written once and not appended to.
550    pub fn write_dataset<T: H5Type>(
551        &mut self,
552        name: &str,
553        dims: &[u64],
554        data: &[T],
555    ) -> Result<usize> {
556        let expected: u64 = if dims.is_empty() {
557            1
558        } else {
559            dims.iter().product()
560        };
561        if data.len() as u64 != expected {
562            return Err(crate::error::Hdf5Error::InvalidState(format!(
563                "write_dataset: data has {} elements but shape {dims:?} needs {expected}",
564                data.len()
565            )));
566        }
567        let idx = self
568            .inner
569            .writer_mut()
570            .create_dataset(name, T::hdf5_type(), dims)?;
571        self.inner
572            .writer_mut()
573            .write_dataset_raw(idx, slice_to_bytes(data))?;
574        Ok(idx)
575    }
576
577    /// [`write_dataset`](Self::write_dataset) with a caller-supplied datatype
578    /// and the raw element bytes, `dims` product times the type's size — for
579    /// NDFileHDF5's NUL-terminated fixed-length string constants, where
580    /// [`write_string_dataset`](Self::write_string_dataset) writes a
581    /// variable-length one. A byte count that does not match is refused
582    /// before the dataset is created.
583    pub fn write_dataset_typed(
584        &mut self,
585        name: &str,
586        datatype: DatatypeMessage,
587        dims: &[u64],
588        data: &[u8],
589    ) -> Result<usize> {
590        let expected = dims.iter().product::<u64>() * datatype.element_size() as u64;
591        if data.len() as u64 != expected {
592            return Err(Hdf5Error::InvalidState(format!(
593                "write_dataset_typed: data has {} bytes but shape {dims:?} needs {expected}",
594                data.len()
595            )));
596        }
597        let idx = self
598            .inner
599            .writer_mut()
600            .create_dataset(name, datatype, dims)?;
601        self.inner.writer_mut().write_dataset_raw(idx, data)?;
602        Ok(idx)
603    }
604
605    /// Create a contiguous dataset of `datatype` and shape `dims` (`&[]`:
606    /// scalar) whose bytes [`write_dataset_raw`](Self::write_dataset_raw)
607    /// supplies later, after [`start_swmr`](Self::start_swmr) too, as
608    /// libhdf5's `H5Dwrite` does under SWMR; until then it reads its fill
609    /// value. Create it before `start_swmr`.
610    pub fn create_dataset_typed(
611        &mut self,
612        name: &str,
613        datatype: DatatypeMessage,
614        dims: &[u64],
615    ) -> Result<usize> {
616        let idx = self
617            .inner
618            .writer_mut()
619            .create_dataset(name, datatype, dims)?;
620        Ok(idx)
621    }
622
623    /// Overwrite every byte of a contiguous dataset made by
624    /// [`create_dataset_typed`](Self::create_dataset_typed), before or after
625    /// [`start_swmr`](Self::start_swmr): raw data only, so SWMR readers see
626    /// it after the next [`flush`](Self::flush).
627    pub fn write_dataset_raw(&mut self, ds_index: usize, data: &[u8]) -> Result<()> {
628        self.inner.writer_mut().write_dataset_raw(ds_index, data)?;
629        Ok(())
630    }
631
632    /// Create a variable-length string dataset (one element per string).
633    /// Returns the dataset index. Useful for NeXus metadata such as
634    /// `/entry/start_time` or per-frame timestamp arrays.
635    ///
636    /// The datatype declares UTF-8;
637    /// [`write_string_dataset_ascii`](Self::write_string_dataset_ascii)
638    /// declares ASCII instead.
639    pub fn write_string_dataset(&mut self, name: &str, strings: &[&str]) -> Result<usize> {
640        let idx = self
641            .inner
642            .writer_mut()
643            .create_vlen_string_dataset(name, strings, 1)?;
644        Ok(idx)
645    }
646
647    /// [`write_string_dataset`](Self::write_string_dataset) under an **ASCII**
648    /// datatype, the one h5py's `string_dtype("ascii")` produces. A string
649    /// that is not 7-bit is rejected rather than mislabelled.
650    pub fn write_string_dataset_ascii(&mut self, name: &str, strings: &[&str]) -> Result<usize> {
651        let idx = self
652            .inner
653            .writer_mut()
654            .create_vlen_string_dataset(name, strings, 0)?;
655        Ok(idx)
656    }
657
658    /// Set a string attribute on a dataset, addressed by its index. The
659    /// NeXus way to record `units`, `long_name`, `signal`, etc. An existing
660    /// attribute of the same name is replaced. After
661    /// [`start_swmr`](Self::start_swmr) it is committed at close — see
662    /// [`set_group_attr_string`](Self::set_group_attr_string).
663    pub fn set_dataset_attr_string(
664        &mut self,
665        ds_index: usize,
666        name: &str,
667        value: &str,
668    ) -> Result<()> {
669        self.inner.writer_mut().set_vlen_string_attribute(
670            crate::io::writer::AttrTarget::Dataset(ds_index),
671            name,
672            value,
673        )?;
674        Ok(())
675    }
676
677    /// Set a numeric scalar attribute on a dataset, addressed by its index.
678    /// An existing attribute of the same name is replaced. After
679    /// [`start_swmr`](Self::start_swmr) it is committed at close — see
680    /// [`set_group_attr_string`](Self::set_group_attr_string).
681    pub fn set_dataset_attr_numeric<T: H5Type>(
682        &mut self,
683        ds_index: usize,
684        name: &str,
685        value: &T,
686    ) -> Result<()> {
687        let attr = AttributeMessage::scalar_numeric(name, T::hdf5_type(), scalar_to_bytes(value));
688        self.inner
689            .writer_mut()
690            .add_dataset_attribute(ds_index, attr)?;
691        Ok(())
692    }
693
694    /// Set a scalar attribute of a caller-supplied datatype on a dataset,
695    /// addressed by its index — the SWMR form of
696    /// [`H5Dataset::set_attr_typed`](crate::H5Dataset::set_attr_typed).
697    /// After [`start_swmr`](Self::start_swmr) it is committed at close — see
698    /// [`set_group_attr_string`](Self::set_group_attr_string).
699    pub fn set_dataset_attr_typed(
700        &mut self,
701        ds_index: usize,
702        name: &str,
703        datatype: DatatypeMessage,
704        value: Vec<u8>,
705    ) -> Result<()> {
706        let attr = crate::attribute::typed_scalar_attr(name, datatype, value)?;
707        self.inner
708            .writer_mut()
709            .add_dataset_attribute(ds_index, attr)?;
710        Ok(())
711    }
712
713    /// Set a numeric array attribute on a dataset, addressed by its index.
714    ///
715    /// `dims` are the dimension sizes (e.g. `&[3]` for a 1-D array) and the
716    /// number of `values` must equal their product. This is the SWMR
717    /// counterpart of [`H5Attribute::write_array`](crate::H5Attribute::write_array),
718    /// for the length-ndims `int32` array attributes AreaDetector writes
719    /// (`NDArrayDimOffset`, `NDArrayDimBinning`, `NDArrayDimReverse`). An
720    /// existing attribute of the same name is replaced.
721    ///
722    /// After [`start_swmr`](Self::start_swmr) it is committed at close — see
723    /// [`set_group_attr_string`](Self::set_group_attr_string). Resolve a
724    /// dataset path to its index with [`dataset_index`](Self::dataset_index).
725    pub fn set_dataset_attr_array<T: H5Type>(
726        &mut self,
727        ds_index: usize,
728        name: &str,
729        dims: &[u64],
730        values: &[T],
731    ) -> Result<()> {
732        let attr = array_attr(name, dims, values)?;
733        self.inner
734            .writer_mut()
735            .add_dataset_attribute(ds_index, attr)?;
736        Ok(())
737    }
738
739    /// Set the fill value of a streaming dataset, addressed by its index.
740    ///
741    /// Call this before the first [`append_frame`](Self::append_frame): it
742    /// determines the value of chunk regions that are never written (a
743    /// partial final band, or unwritten tiles).
744    pub fn set_dataset_fill_value<T: H5Type>(&mut self, ds_index: usize, value: &T) -> Result<()> {
745        self.set_dataset_fill_value_bytes(ds_index, &scalar_to_bytes(value))
746    }
747
748    /// [`set_dataset_fill_value`](Self::set_dataset_fill_value) from one
749    /// element encoded in the dataset's stored datatype — for a type no `T`
750    /// carries, such as one made with a `_with_datatype` constructor.
751    /// `bytes` is the element's host-order image, `datatype.element_size()`
752    /// bytes long; it is put in the order the datatype declares.
753    pub fn set_dataset_fill_value_bytes(&mut self, ds_index: usize, bytes: &[u8]) -> Result<()> {
754        let writer = self.inner.writer_mut();
755        if ds_index >= writer.dataset_count() {
756            return Err(Hdf5Error::InvalidState(format!(
757                "dataset index {ds_index} out of range"
758            )));
759        }
760        let datatype = writer.ds(ds_index).lock().datatype.clone();
761        let width = datatype.element_size() as usize;
762        if bytes.len() != width {
763            return Err(Hdf5Error::TypeMismatch(format!(
764                "fill value is {} bytes but datatype {datatype} stores {width}",
765                bytes.len()
766            )));
767        }
768        let stored = crate::dataset::to_stored_byte_order(bytes, &datatype, width)?;
769        writer.set_dataset_fill_value(ds_index, stored.into_owned())?;
770        Ok(())
771    }
772
773    /// Place an existing dataset inside a group.
774    ///
775    /// By default a dataset created through this writer lives at the root
776    /// level; this moves its link record into `group_path` (which must
777    /// already exist). The group must be created before `start_swmr` for the
778    /// placement to be visible to readers during streaming.
779    pub fn assign_dataset_to_group(&mut self, group_path: &str, ds_index: usize) -> Result<()> {
780        self.inner
781            .writer_mut()
782            .assign_dataset_to_group(group_path, ds_index)?;
783        Ok(())
784    }
785
786    /// Signal the start of SWMR mode.
787    pub fn start_swmr(&mut self) -> Result<()> {
788        self.inner.start_swmr()?;
789        Ok(())
790    }
791
792    /// Append a frame of raw data to a streaming dataset.
793    ///
794    /// The data size must match one frame (product of frame_dims * element_size).
795    pub fn append_frame(&mut self, ds_index: usize, data: &[u8]) -> Result<()> {
796        self.inner.append_frame(ds_index, data)?;
797        Ok(())
798    }
799
800    /// Append a frame that is already filtered (e.g. compressed by a codec
801    /// plugin upstream), storing its bytes verbatim — HDF5's direct chunk
802    /// write (`H5Dwrite_chunk`) for a streaming dataset.
803    ///
804    /// `data` is the stored (compressed) image of one frame; the dataset's
805    /// filter pipeline, recorded when it was created with a `_compressed`
806    /// constructor, is what readers run to decode it. `filter_mask` bit *i*
807    /// set means filter *i* was not applied to this frame; pass 0 when the
808    /// whole pipeline was. The dataset must store one whole frame per chunk
809    /// ([`create_streaming_dataset_compressed`](Self::create_streaming_dataset_compressed),
810    /// or a `_chunked_compressed` or filtered `_with_max_frames` chunk of
811    /// `[1, frame_dims...]`).
812    pub fn append_frame_raw(
813        &mut self,
814        ds_index: usize,
815        data: &[u8],
816        filter_mask: u32,
817    ) -> Result<()> {
818        self.inner.append_frame_raw(ds_index, data, filter_mask)?;
819        Ok(())
820    }
821
822    /// Write one already-filtered chunk verbatim at an explicit chunk
823    /// position of a filtered grid dataset made by
824    /// [`create_grid_dataset_compressed`](Self::create_grid_dataset_compressed)
825    /// or [`create_grid_dataset_with_max`](Self::create_grid_dataset_with_max)
826    /// — HDF5's direct chunk write (`H5Dwrite_chunk`) at a chunk offset.
827    /// `filter_mask` bit *i* set means filter *i* was not applied to this
828    /// chunk; pass 0 when the whole pipeline was.
829    pub fn write_chunk_raw_at(
830        &mut self,
831        ds_index: usize,
832        chunk_coords: &[u64],
833        data: &[u8],
834        filter_mask: u32,
835    ) -> Result<()> {
836        self.inner
837            .write_chunk_raw_at(ds_index, chunk_coords, data, filter_mask)?;
838        Ok(())
839    }
840
841    /// Write one frame at an explicit chunk position of a grid dataset
842    /// created with [`create_grid_dataset`](Self::create_grid_dataset) or
843    /// [`create_grid_dataset_with_max`](Self::create_grid_dataset_with_max).
844    ///
845    /// `chunk_coords` are in units of chunks (row-major over the chunk grid)
846    /// and `data` must be exactly one full chunk (`product(chunk) *
847    /// element_size` bytes; edge chunks are zero-padded by the caller).
848    /// Positions may be written in any order, beyond the extent too while
849    /// inside the maximum shape, and unwritten positions read back as fill.
850    /// As with the streaming path, call [`flush`](Self::flush) to make writes
851    /// visible to SWMR readers and set dataset attributes before
852    /// [`start_swmr`](Self::start_swmr).
853    pub fn write_chunk_at(
854        &mut self,
855        ds_index: usize,
856        chunk_coords: &[u64],
857        data: &[u8],
858    ) -> Result<()> {
859        self.inner.write_chunk_at(ds_index, chunk_coords, data)?;
860        Ok(())
861    }
862
863    /// Write a hyperslab (`starts`, `counts`, in elements) of a grid dataset
864    /// inside its extent. A chunk the selection covers only in part keeps
865    /// its other elements, or their fill if it was never written, so frames
866    /// sharing a chunk may arrive in any order. `data` is `product(counts) *
867    /// element_size` bytes. Call [`flush`](Self::flush) to make it visible.
868    pub fn write_slice(
869        &mut self,
870        ds_index: usize,
871        starts: &[u64],
872        counts: &[u64],
873        data: &[u8],
874    ) -> Result<()> {
875        self.inner.write_slice(ds_index, starts, counts, data)?;
876        Ok(())
877    }
878
879    /// Set a dataset's logical extent within its maximum shape, growing or
880    /// shrinking it — libhdf5's `H5Dset_extent`, e.g. a grid's extent
881    /// following the positions written so far. A multi-frame-chunk stream,
882    /// whose extent counts the frames it buffers, is refused.
883    pub fn set_extent(&mut self, ds_index: usize, dims: &[u64]) -> Result<()> {
884        self.inner.set_extent(ds_index, dims)?;
885        Ok(())
886    }
887
888    /// Flush all dataset index structures to disk with SWMR ordering.
889    pub fn flush(&mut self) -> Result<()> {
890        self.inner.flush()?;
891        Ok(())
892    }
893
894    /// Close and finalize the file.
895    pub fn close(self) -> Result<()> {
896        self.inner.close()?;
897        Ok(())
898    }
899}
900
901/// SWMR reader for monitoring a streaming HDF5 file.
902///
903/// Opens a file being written by a concurrent [`SwmrFileWriter`] and
904/// periodically calls [`refresh`](Self::refresh) to pick up new data.
905///
906/// ```no_run
907/// use rust_hdf5::swmr::SwmrFileReader;
908///
909/// let mut reader = SwmrFileReader::open("stream.h5").unwrap();
910///
911/// loop {
912///     reader.refresh().unwrap();
913///     let names = reader.dataset_names();
914///     if let Some(shape) = reader.dataset_shape("frames").ok() {
915///         println!("frames shape: {:?}", shape);
916///         if shape[0] > 0 {
917///             let data = reader.read_dataset_raw("frames").unwrap();
918///             println!("got {} bytes", data.len());
919///             break;
920///         }
921///     }
922///     std::thread::sleep(std::time::Duration::from_millis(100));
923/// }
924/// ```
925pub struct SwmrFileReader {
926    reader: Hdf5Reader,
927}
928
929impl SwmrFileReader {
930    /// Open an HDF5 file for SWMR reading using the env-var-derived
931    /// locking policy. Takes a shared lock so it coexists with the
932    /// downgraded shared lock held by [`SwmrFileWriter`] after
933    /// `start_swmr`, and with other concurrent SWMR readers.
934    pub fn open<P: AsRef<Path>>(path: P) -> Result<Self> {
935        let reader = Hdf5Reader::open_swmr(path.as_ref())?;
936        Ok(Self { reader })
937    }
938
939    /// Open an HDF5 file for SWMR reading with an explicit locking policy.
940    pub fn open_with_locking<P: AsRef<Path>>(path: P, locking: FileLocking) -> Result<Self> {
941        let reader = Hdf5Reader::open_swmr_with_locking(path.as_ref(), locking)?;
942        Ok(Self { reader })
943    }
944
945    /// Re-read the superblock and dataset metadata from disk.
946    ///
947    /// Call this periodically to pick up new data written by the concurrent
948    /// SWMR writer.
949    pub fn refresh(&mut self) -> Result<()> {
950        self.reader.refresh()?;
951        Ok(())
952    }
953
954    /// Return the names of all datasets.
955    pub fn dataset_names(&self) -> Vec<String> {
956        self.reader
957            .dataset_names()
958            .iter()
959            .map(|s| s.to_string())
960            .collect()
961    }
962
963    /// Return the current shape of a dataset.
964    pub fn dataset_shape(&mut self, name: &str) -> Result<Vec<u64>> {
965        Ok(self.reader.dataset_shape(name)?)
966    }
967
968    /// Read the raw bytes of a dataset.
969    pub fn read_dataset_raw(&mut self, name: &str) -> Result<Vec<u8>> {
970        Ok(self.reader.read_dataset_raw(name)?)
971    }
972
973    /// Read a dataset as a typed vector.
974    ///
975    /// `T` must have the dataset's exact element width — the same
976    /// width-checked byte reinterpretation as
977    /// [`H5Dataset::read_raw`](crate::dataset::H5Dataset::read_raw), with no
978    /// datatype conversion. Use
979    /// [`dataset_element_size`](Self::dataset_element_size) to size-check a
980    /// type at runtime.
981    pub fn read_dataset<T: H5Type>(&mut self, name: &str) -> Result<Vec<T>> {
982        self.check_element_width::<T>(name)?;
983        let datatype = self.dataset_datatype(name)?;
984        let es = T::element_size();
985        let total = self.reader.dataset_raw_size(name)? as usize;
986        if es == 0 || !total.is_multiple_of(es) {
987            return Err(crate::error::Hdf5Error::TypeMismatch(format!(
988                "raw data size {total} is not a multiple of element size {es}"
989            )));
990        }
991        // As the non-SWMR full read: the image lands in the vector this
992        // returns rather than in a byte buffer copied into it afterwards.
993        crate::io::reader::read_image_into_new(total / es, |image| {
994            self.reader.read_dataset_raw_into_dst(
995                name,
996                image,
997                crate::io::file_handle::ReadDst::Fresh,
998            )?;
999            crate::dataset::to_host_byte_order(image, &datatype, es)
1000        })
1001    }
1002
1003    /// Read a slice (hyperslab) of a dataset as raw bytes.
1004    ///
1005    /// `starts[d]` is the first index along dimension `d`, `counts[d]` is how
1006    /// many. For a streaming dataset this reads only the chunks the slice
1007    /// overlaps — the efficient way for a live viewer to fetch the latest
1008    /// frame without re-reading the whole stream.
1009    pub fn read_slice_raw(
1010        &mut self,
1011        name: &str,
1012        starts: &[u64],
1013        counts: &[u64],
1014    ) -> Result<Vec<u8>> {
1015        Ok(self.reader.read_slice(name, starts, counts)?)
1016    }
1017
1018    /// Read a slice (hyperslab) of a dataset as a typed vector.
1019    /// See [`read_slice_raw`](Self::read_slice_raw); `T`'s width is checked
1020    /// like [`read_dataset`](Self::read_dataset).
1021    pub fn read_slice<T: H5Type>(
1022        &mut self,
1023        name: &str,
1024        starts: &[u64],
1025        counts: &[u64],
1026    ) -> Result<Vec<T>> {
1027        self.check_element_width::<T>(name)?;
1028        let datatype = self.dataset_datatype(name)?;
1029        bytes_to_typed(self.reader.read_slice(name, starts, counts)?, &datatype)
1030    }
1031
1032    /// The width gate for the typed reads: without it, reinterpreting the
1033    /// raw bytes splits or merges elements — an f64 dataset read as `i32`
1034    /// passed the divisibility check and silently returned twice as many
1035    /// garbage values.
1036    fn check_element_width<T: H5Type>(&mut self, name: &str) -> Result<()> {
1037        let stored = self.dataset_element_size(name)?;
1038        if T::element_size() != stored {
1039            return Err(crate::error::Hdf5Error::TypeMismatch(format!(
1040                "read type has element size {} but dataset has element size {stored}",
1041                T::element_size(),
1042            )));
1043        }
1044        Ok(())
1045    }
1046
1047    /// Read a variable-length string dataset.
1048    pub fn read_vlen_strings(&mut self, name: &str) -> Result<Vec<String>> {
1049        Ok(self.reader.read_vlen_strings(name)?)
1050    }
1051
1052    /// Element size of a dataset's datatype, in bytes — enough to size a
1053    /// read buffer without knowing the concrete element type at compile time.
1054    pub fn dataset_element_size(&mut self, name: &str) -> Result<usize> {
1055        self.reader
1056            .dataset_info(name)
1057            .map(|i| i.datatype.element_size() as usize)
1058            .ok_or_else(|| crate::error::Hdf5Error::NotFound(name.to_string()))
1059    }
1060
1061    /// The stored datatype of a dataset, which the typed reads need to put
1062    /// the element image into host byte order.
1063    fn dataset_datatype(&mut self, name: &str) -> Result<DatatypeMessage> {
1064        self.reader
1065            .dataset_info(name)
1066            .map(|i| i.datatype.clone())
1067            .ok_or_else(|| crate::error::Hdf5Error::NotFound(name.to_string()))
1068    }
1069
1070    /// All group paths in the file.
1071    pub fn group_paths(&self) -> Vec<String> {
1072        self.reader.group_paths().iter().cloned().collect()
1073    }
1074
1075    /// Whether a group exists. A leading `/` is tolerated.
1076    pub fn has_group(&self, group_path: &str) -> bool {
1077        self.reader.has_group(group_path.trim_start_matches('/'))
1078    }
1079
1080    /// Names of the attributes on a dataset.
1081    pub fn dataset_attr_names(&mut self, name: &str) -> Result<Vec<String>> {
1082        Ok(self.reader.dataset_attr_names(name)?)
1083    }
1084
1085    /// Read a dataset's attribute as a string (e.g. `units`, `NX_class`).
1086    pub fn dataset_attr_string(&mut self, dataset: &str, attr: &str) -> Result<String> {
1087        let a = self.reader.dataset_attr(dataset, attr)?.clone();
1088        Ok(self.reader.attr_string_value(&a)?)
1089    }
1090
1091    /// Names of the attributes on a group, or on the root group when
1092    /// `group_path` is `"/"`. A leading `/` is tolerated.
1093    pub fn group_attr_names(&mut self, group_path: &str) -> Result<Vec<String>> {
1094        Ok(if group_path == "/" {
1095            self.reader.root_attr_names()?
1096        } else {
1097            self.reader
1098                .group_attr_names(group_path.trim_start_matches('/'))?
1099        })
1100    }
1101
1102    /// Read a group's attribute as a string (the NeXus `NX_class` etc.), or
1103    /// a root attribute when `group_path` is `"/"`. A leading `/` is tolerated.
1104    pub fn group_attr_string(&mut self, group_path: &str, attr: &str) -> Result<String> {
1105        let a = if group_path == "/" {
1106            self.reader.root_attr(attr)
1107        } else {
1108            self.reader
1109                .group_attr(group_path.trim_start_matches('/'), attr)
1110        }?
1111        .clone();
1112        Ok(self.reader.attr_string_value(&a)?)
1113    }
1114}
1115
1116/// The writer-side attribute list a group path names: `"/"` is the
1117/// file-level (root) list, anything else the group's own.
1118fn group_attr_target(group_path: &str) -> crate::io::writer::AttrTarget<'_> {
1119    if group_path == "/" {
1120        crate::io::writer::AttrTarget::Root
1121    } else {
1122        crate::io::writer::AttrTarget::Group(group_path)
1123    }
1124}
1125
1126/// Reinterpret a raw byte buffer as a typed vector. The buffer length must
1127/// be a whole multiple of `T`'s element size, and the stored byte order is
1128/// converted to the host's first — reinterpretation is only the stored value
1129/// when the two agree.
1130fn bytes_to_typed<T: H5Type>(mut raw: Vec<u8>, datatype: &DatatypeMessage) -> Result<Vec<T>> {
1131    let es = T::element_size();
1132    if es == 0 || !raw.len().is_multiple_of(es) {
1133        return Err(crate::error::Hdf5Error::TypeMismatch(format!(
1134            "raw data size {} is not a multiple of element size {es}",
1135            raw.len()
1136        )));
1137    }
1138    crate::dataset::to_host_byte_order(&mut raw, datatype, es)?;
1139    let count = raw.len() / es;
1140    let mut result = Vec::<T>::with_capacity(count);
1141    // Safety: `T: H5Type` is a `Copy` POD primitive exactly `element_size()`
1142    // bytes wide, so the byte buffer is a valid array of `count` `T`s.
1143    unsafe {
1144        std::ptr::copy_nonoverlapping(raw.as_ptr(), result.as_mut_ptr() as *mut u8, raw.len());
1145        result.set_len(count);
1146    }
1147    Ok(result)
1148}
1149
1150/// The array attribute message for `values` of shape `dims`, whose product
1151/// must be the value count (an empty shape is a scalar, one value).
1152fn array_attr<T: H5Type>(name: &str, dims: &[u64], values: &[T]) -> Result<AttributeMessage> {
1153    let expected: u64 = dims.iter().product();
1154    if values.len() as u64 != expected {
1155        return Err(crate::error::Hdf5Error::InvalidState(format!(
1156            "attribute '{name}': {} values but shape {dims:?} needs {expected}",
1157            values.len()
1158        )));
1159    }
1160    Ok(AttributeMessage::array_numeric(
1161        name,
1162        T::hdf5_type(),
1163        dims,
1164        slice_to_bytes(values).to_vec(),
1165    ))
1166}
1167
1168/// Raw bytes of one `H5Type` scalar.
1169fn scalar_to_bytes<T: H5Type>(value: &T) -> Vec<u8> {
1170    let es = T::element_size();
1171    // Safety: `T: H5Type` is a `Copy` POD primitive exactly `element_size()`
1172    // bytes wide.
1173    unsafe { std::slice::from_raw_parts(value as *const T as *const u8, es) }.to_vec()
1174}
1175
1176/// Raw bytes of an `H5Type` slice, in element order.
1177fn slice_to_bytes<T: H5Type>(data: &[T]) -> &[u8] {
1178    // Safety: as `scalar_to_bytes`; the slice is a contiguous POD array.
1179    unsafe { std::slice::from_raw_parts(data.as_ptr() as *const u8, std::mem::size_of_val(data)) }
1180}