feat(wasm): clawhdf5-wasm, the reader for JavaScript via wasm-bindgen
open(bytes) -> H5File with kind/list/info/attrs/attrErrors/read/ readHyperslab. Numeric data comes back in the typed array of the stored width (Int16Array for i16, BigInt64Array for i64, Float32Array for f32/f16, ...), strings and enum names as string arrays, array datatypes flattened with their dims appended to the shape. Compound, reference, opaque and VL-sequence datasets are refused with an error naming the type; nothing is returned as reinterpreted bytes. The logic is in a plain-Rust core module, tested natively: unit tests, and h5py_interop, which compares every dataset, hyperslab, listing and attribute of an h5py- and a netCDF4-written file with what libhdf5 reads back (generator shared with the Node test of the built package). No mmap, no threads; lz4 is on, zstd/szip (C) are not. A wasm-release profile (opt-level s, LTO) serves the browser build. ci-test.sh lints the crate for wasm32 and checks it builds no C. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
@@ -0,0 +1,244 @@
|
||||
//! clawhdf5's HDF5 reader for JavaScript, via `wasm-bindgen`.
|
||||
//!
|
||||
//! ```js
|
||||
//! import init, { open } from "./pkg/clawhdf5_wasm.js";
|
||||
//! await init();
|
||||
//! const file = open(new Uint8Array(await blob.arrayBuffer()));
|
||||
//! file.list("/"); // [{ name, kind: "group" | "dataset" }]
|
||||
//! file.info("/x"); // { shape, maxshape, dtype, elementShape }
|
||||
//! file.attrs("/x"); // [{ name, value, dtype }]
|
||||
//! file.read("/x"); // { shape, dtype, data: Float64Array | ... | string[] }
|
||||
//! file.readHyperslab("/x", [0, 0], [10, 10]); // stride, block optional
|
||||
//! file.free();
|
||||
//! ```
|
||||
//!
|
||||
//! Numeric data comes back in the typed array of the stored width
|
||||
//! (`Int16Array` for `i16`, `BigInt64Array` for `i64`, `Float32Array` for
|
||||
//! `f32` and `f16`, ...); strings and enumeration names as arrays of strings.
|
||||
//! Anything else is a thrown `Error` naming the datatype. Only the reader is
|
||||
//! exposed: nothing here writes files.
|
||||
//!
|
||||
//! The logic lives in [`core`], which is plain Rust and tested natively.
|
||||
|
||||
pub mod core;
|
||||
|
||||
use clawhdf5::AttrValue;
|
||||
use js_sys::{Array, Object, Reflect};
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
use crate::core::{Data, Hyperslab, Reader};
|
||||
|
||||
/// JavaScript numbers are exact up to 2^53.
|
||||
const MAX_SAFE_INTEGER: f64 = 9_007_199_254_740_991.0;
|
||||
|
||||
fn js_err(msg: String) -> JsError {
|
||||
JsError::new(&msg)
|
||||
}
|
||||
|
||||
fn set(obj: &Object, key: &str, value: impl Into<JsValue>) {
|
||||
// Defining a property on a fresh plain object cannot fail.
|
||||
Reflect::set(obj, &JsValue::from_str(key), &value.into()).unwrap_throw();
|
||||
}
|
||||
|
||||
fn shape_to_js(shape: &[u64]) -> Array {
|
||||
shape.iter().map(|&d| JsValue::from_f64(d as f64)).collect()
|
||||
}
|
||||
|
||||
fn indices_from_js(what: &str, v: &[f64]) -> Result<Vec<u64>, JsError> {
|
||||
v.iter()
|
||||
.map(|&x| {
|
||||
if x.is_finite() && x >= 0.0 && x.fract() == 0.0 && x <= MAX_SAFE_INTEGER {
|
||||
Ok(x as u64)
|
||||
} else {
|
||||
Err(js_err(format!(
|
||||
"{what} must hold non-negative integers, got {x}"
|
||||
)))
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn data_to_js(data: Data) -> JsValue {
|
||||
match data {
|
||||
Data::F32(v) => js_sys::Float32Array::from(&v[..]).into(),
|
||||
Data::F64(v) => js_sys::Float64Array::from(&v[..]).into(),
|
||||
Data::I8(v) => js_sys::Int8Array::from(&v[..]).into(),
|
||||
Data::I16(v) => js_sys::Int16Array::from(&v[..]).into(),
|
||||
Data::I32(v) => js_sys::Int32Array::from(&v[..]).into(),
|
||||
Data::I64(v) => js_sys::BigInt64Array::from(&v[..]).into(),
|
||||
Data::U8(v) => js_sys::Uint8Array::from(&v[..]).into(),
|
||||
Data::U16(v) => js_sys::Uint16Array::from(&v[..]).into(),
|
||||
Data::U32(v) => js_sys::Uint32Array::from(&v[..]).into(),
|
||||
Data::U64(v) => js_sys::BigUint64Array::from(&v[..]).into(),
|
||||
Data::Strings(v) => v
|
||||
.into_iter()
|
||||
.map(|s| JsValue::from_str(&s))
|
||||
.collect::<Array>()
|
||||
.into(),
|
||||
}
|
||||
}
|
||||
|
||||
/// A scalar integer as a `number` when exact, else a `bigint`.
|
||||
fn int_to_js(x: i128) -> JsValue {
|
||||
if (x as f64).abs() <= MAX_SAFE_INTEGER {
|
||||
JsValue::from_f64(x as f64)
|
||||
} else if let Ok(v) = i64::try_from(x) {
|
||||
JsValue::from(v)
|
||||
} else {
|
||||
JsValue::from(x as u64)
|
||||
}
|
||||
}
|
||||
|
||||
/// An attribute value, and the datatype of one that has no JavaScript form.
|
||||
fn attr_to_js(value: AttrValue) -> (JsValue, Option<String>) {
|
||||
match value {
|
||||
AttrValue::F64(x) => (JsValue::from_f64(x), None),
|
||||
AttrValue::F64Array(v) => (js_sys::Float64Array::from(&v[..]).into(), None),
|
||||
AttrValue::I64(x) => (int_to_js(i128::from(x)), None),
|
||||
AttrValue::I64Array(v) => (js_sys::BigInt64Array::from(&v[..]).into(), None),
|
||||
AttrValue::U64(x) => (int_to_js(i128::from(x)), None),
|
||||
AttrValue::U64Array(v) => (js_sys::BigUint64Array::from(&v[..]).into(), None),
|
||||
AttrValue::String(s) => (JsValue::from_str(&s), None),
|
||||
AttrValue::StringArray(v) => (
|
||||
v.iter()
|
||||
.map(|s| JsValue::from_str(s))
|
||||
.collect::<Array>()
|
||||
.into(),
|
||||
None,
|
||||
),
|
||||
AttrValue::Raw { datatype, .. } => (JsValue::NULL, Some(core::describe(&datatype))),
|
||||
}
|
||||
}
|
||||
|
||||
/// An open HDF5 (or NetCDF-4) file.
|
||||
#[wasm_bindgen]
|
||||
pub struct H5File {
|
||||
inner: Reader,
|
||||
}
|
||||
|
||||
/// Open a file from its bytes. Throws if they are not an HDF5 file.
|
||||
#[wasm_bindgen]
|
||||
pub fn open(bytes: Vec<u8>) -> Result<H5File, JsError> {
|
||||
H5File::new(bytes)
|
||||
}
|
||||
|
||||
/// The clawhdf5 version this module was built from.
|
||||
#[wasm_bindgen]
|
||||
pub fn version() -> String {
|
||||
env!("CARGO_PKG_VERSION").to_string()
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl H5File {
|
||||
/// Same as [`open`].
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new(bytes: Vec<u8>) -> Result<H5File, JsError> {
|
||||
Ok(H5File {
|
||||
inner: Reader::open(bytes).map_err(js_err)?,
|
||||
})
|
||||
}
|
||||
|
||||
/// `"group"` or `"dataset"`.
|
||||
pub fn kind(&self, path: &str) -> Result<String, JsError> {
|
||||
Ok(self.inner.kind(path).map_err(js_err)?.as_str().to_string())
|
||||
}
|
||||
|
||||
/// The group's members: `[{ name, kind }]`, groups first.
|
||||
pub fn list(&self, path: &str) -> Result<Array, JsError> {
|
||||
Ok(self
|
||||
.inner
|
||||
.list(path)
|
||||
.map_err(js_err)?
|
||||
.into_iter()
|
||||
.map(|c| {
|
||||
let o = Object::new();
|
||||
set(&o, "name", c.name);
|
||||
set(&o, "kind", c.kind.as_str());
|
||||
JsValue::from(o)
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
/// `{ shape, maxshape, dtype, elementShape }`. `maxshape` is `null`
|
||||
/// when not recorded, with `null` for each unlimited dimension.
|
||||
pub fn info(&self, path: &str) -> Result<Object, JsError> {
|
||||
let i = self.inner.info(path).map_err(js_err)?;
|
||||
let o = Object::new();
|
||||
set(&o, "shape", shape_to_js(&i.shape));
|
||||
let max: JsValue = match i.maxshape {
|
||||
None => JsValue::NULL,
|
||||
Some(dims) => dims
|
||||
.into_iter()
|
||||
.map(|d| d.map_or(JsValue::NULL, |d| JsValue::from_f64(d as f64)))
|
||||
.collect::<Array>()
|
||||
.into(),
|
||||
};
|
||||
set(&o, "maxshape", max);
|
||||
set(&o, "dtype", i.dtype);
|
||||
set(&o, "elementShape", shape_to_js(&i.element_shape));
|
||||
Ok(o)
|
||||
}
|
||||
|
||||
/// `[{ name, value, dtype }]`, sorted by name. Scalars are `number`
|
||||
/// (`bigint` beyond 2^53) or `string`; arrays are typed arrays or
|
||||
/// `string[]`. An attribute with no JavaScript form has `value: null`
|
||||
/// and its `dtype`; one that could not be read at all is reported by
|
||||
/// [`attrErrors`](Self::attr_errors).
|
||||
pub fn attrs(&self, path: &str) -> Result<Array, JsError> {
|
||||
let (attrs, _) = self.inner.attrs(path).map_err(js_err)?;
|
||||
Ok(attrs
|
||||
.into_iter()
|
||||
.map(|a| {
|
||||
let o = Object::new();
|
||||
set(&o, "name", a.name);
|
||||
let (value, dtype) = attr_to_js(a.value);
|
||||
set(&o, "value", value);
|
||||
set(&o, "dtype", dtype.map_or(JsValue::NULL, JsValue::from));
|
||||
JsValue::from(o)
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
/// Messages for attributes that could not be read.
|
||||
#[wasm_bindgen(js_name = attrErrors)]
|
||||
pub fn attr_errors(&self, path: &str) -> Result<Array, JsError> {
|
||||
let (_, errors) = self.inner.attrs(path).map_err(js_err)?;
|
||||
Ok(errors.into_iter().map(JsValue::from).collect())
|
||||
}
|
||||
|
||||
/// The whole dataset: `{ shape, dtype, data }`, `data` in row-major
|
||||
/// order.
|
||||
pub fn read(&self, path: &str) -> Result<Object, JsError> {
|
||||
self.read_impl(path, None)
|
||||
}
|
||||
|
||||
/// A regular hyperslab (`H5Sselect_hyperslab`): `stride` and `block`
|
||||
/// default to 1. The result's shape is `count * block` per dimension.
|
||||
#[wasm_bindgen(js_name = readHyperslab)]
|
||||
pub fn read_hyperslab(
|
||||
&self,
|
||||
path: &str,
|
||||
start: Vec<f64>,
|
||||
count: Vec<f64>,
|
||||
stride: Option<Vec<f64>>,
|
||||
block: Option<Vec<f64>>,
|
||||
) -> Result<Object, JsError> {
|
||||
let slab = Hyperslab {
|
||||
start: indices_from_js("start", &start)?,
|
||||
count: indices_from_js("count", &count)?,
|
||||
stride: stride.map(|s| indices_from_js("stride", &s)).transpose()?,
|
||||
block: block.map(|b| indices_from_js("block", &b)).transpose()?,
|
||||
};
|
||||
self.read_impl(path, Some(&slab))
|
||||
}
|
||||
|
||||
fn read_impl(&self, path: &str, slab: Option<&Hyperslab>) -> Result<Object, JsError> {
|
||||
let dtype = self.inner.info(path).map_err(js_err)?.dtype;
|
||||
let a = self.inner.read(path, slab).map_err(js_err)?;
|
||||
let o = Object::new();
|
||||
set(&o, "shape", shape_to_js(&a.shape));
|
||||
set(&o, "dtype", dtype);
|
||||
set(&o, "data", data_to_js(a.data));
|
||||
Ok(o)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user