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}