#ifndef __MAT_H5_HPP__ #define __MAT_H5_HPP__ #include #include #include #include #include namespace jdi { /** * @brief 只读的 HDF5 子集解析器(用于 MATLAB 7.3 的 `.mat` 文件)。 * * MATLAB 7.3(`-v7.3`)的 `.mat` 其实是 **HDF5** 文件:数值矩阵是数据集, * 结构体是分组(字段 = 成员),元信息放在属性里(如 `MATLAB_class`)。 * 本项目不引入 HDF5 库,因此这里实现一个**刚好够用**的只读子集: * * * 超级块 v0/v1/v2/v3; * * 对象头 v1(含续块消息),v2 仅识别并跳过; * * 分组成员:符号表(B-tree v1 + 本地堆)与 link 消息两种方式; * * 数据空间 v1/v2、数据类型 v1(整数 / 浮点 / 定长字符串 / 引用 / 复合 / vlen); * * 数据布局 v3(紧凑 / 连续 / 分块),分块数据集支持 deflate 压缩(zlib)。 * * ⚠️ 不支持:外部数据文件、共享消息表、非 deflate 过滤器、极老的 v1 布局。 * 这些在 MATLAB 保存的文件里都不出现。 */ class MatHdf5 { public: /** @brief 数据类型类别(对应 HDF5 的 class 编号)。 */ enum class TypeClass : int { Integer = 0, Float = 1, Time = 2, String = 3, Bitfield = 4, Opaque = 5, Compound = 6, Reference = 7, Enum = 8, VariableLength = 9, Array = 10 }; /** @brief 数据类型(只保留读取 MATLAB 数据所需的字段)。 */ struct Type { TypeClass cls = TypeClass::Opaque; int version = 1; std::size_t size = 0; /**< 单个元素占用的字节数。 */ bool isSigned = false; /**< 整数是否有符号。 */ bool bigEndian = false; /**< 字节序(MATLAB 一律小端)。 */ bool isVariableString = false; /**< vlen 字符串。 */ std::vector> members; /**< 复合类型成员。 */ }; /** @brief 数据布局。 */ enum class LayoutClass { Compact = 0, Contiguous = 1, Chunked = 2 }; /** @brief 一个数据集的定义与数据。 */ struct Dataset { std::vector dims; /**< 各维长度。 */ Type type; /**< 元素类型。 */ LayoutClass layout = LayoutClass::Compact; std::vector data; /**< 解码后的原始字节(行优先,按 `dims`)。 */ std::uint64_t chunkAddress = 0; /**< 分块数据集的 B-tree 地址。 */ std::vector chunkDims; bool deflate = false; /**< 是否使用了 deflate(zlib)过滤器。 */ std::uint64_t dataAddress = 0; /**< 连续布局的数据起始地址(已加基地址)。 */ std::uint64_t dataBytes = 0; /**< 连续布局的数据字节数。 */ /** 紧凑布局的字节(在布局消息里,数据量很小)。 */ std::vector compact; bool ok = false; /** @brief 元素总数(各维长度相乘)。 */ std::uint64_t ElementCount() const { std::uint64_t count = 1; for (const std::size_t dim : dims) { count *= dim; } return dims.empty() ? 0 : count; } }; /** @brief 读文件进度回调:`已读字节 / 总字节`。返回 false 表示请求取消。 */ using OpenProgress = std::function; /** * @brief 打开文件(**随机读取**,不映射、不把整个文件读进堆内存)。 * * 早期版本把整个文件一次读入 `std::vector`,后来改成整文件内存映射; * 但映射在「文件被截断 / 网络共享掉线 / 移动介质拔出」时会触发页访问异常, * 而这类异常属于 SEH,GCC/MinGW 下的 `try/catch` **捕获不到**,只能让进程崩溃。 * * 因此这里改为 SEC2 式的「`ReadFile` 随机读 + 元数据块缓存」: * * 所有读取都经过 `ReadAt`,I/O 失败只会变成 `false`(上层给出错误提示); * * 对象头 / B-tree / 本地堆等热点元数据走 64KB 块缓存(LRU); * * 数据段按 4MB 切片顺序读,并对小 chunk 做顺序预读。 * 解析代码本身与映射版完全一致。 * * @param [IN] path 文件路径。 * @param [OUT] error 失败原因。 * @param [IN] progress 打开阶段的进度回调(可为空,只在上报总大小时调用一次)。 * @return 成功返回 true;用户取消时也返回 false。 */ bool Open(const std::wstring& path, std::wstring& error, const OpenProgress& progress = OpenProgress()); void Close(); /** @brief 析构时关闭文件句柄。 */ ~MatHdf5() { Close(); } /** @brief 文件字节数。 */ std::uint64_t FileBytes() const { return m_size; } /** @brief 已从文件实际读取的字节数(用于「已读 / 总大小」进度)。 */ std::uint64_t BytesRead() const { return m_bytesRead; } /** * @brief 流式解码期间的字节进度回调。 * * 每读一个块(或每 4MB 切片)调用一次;返回 false 表示请求取消。 */ using ReadProgress = std::function; /** @brief 设置流式解码期间的字节进度回调(可为空)。 */ void SetReadProgress(const ReadProgress& progress) const { m_readProgress = progress; } /** @brief 流式解码是否已被进度回调取消。 */ bool Cancelled() const { return m_readCancelled; } /** @brief 根分组地址。 */ std::uint64_t Root() const { return m_root; } /** @brief 该对象是否为数据集(有数据空间消息)。 */ bool IsDataset(std::uint64_t address) const; /** @brief 分组(或结构体)的成员:`名字 → 对象头地址`。 */ std::vector> Members(std::uint64_t address) const; /** @brief 按名字取成员(不存在返回 0)。 */ std::uint64_t Member(std::uint64_t address, const std::string& name) const; /** @brief 读取数据集定义(含解码后的原始数据)。 */ bool ReadDataset(std::uint64_t address, Dataset& out) const; /** @brief 只读取数据集定义(不解码数据;大数据用 `StreamDataset`)。 */ bool ReadDatasetInfo(std::uint64_t address, Dataset& out) const; /** * @brief 字节流回调:把数据集内容按块吐出(**不生成整块缓冲**)。 * @param [IN] bytes 块起始指针(仅在回调期间有效)。 * @param [IN] byteCount 块字节数。 * @param [IN] elementOffset 该块在数据集中的起始**元素**下标(行优先)。 * @return 返回 false 中止遍历。 */ using ByteSink = std::function; /** * @brief 流式遍历数据集字节(连续布局按 4MB 切片;分块布局逐个 chunk 解压)。 * @param [IN] dataset `ReadDatasetInfo` 得到的信息。 * @param [IN] sink 字节回调。 * @return 成功返回 true。 */ bool StreamDataset(const Dataset& dataset, const ByteSink& sink) const; /** @brief 读取字符串属性(如 `MATLAB_class`、`MATLAB_fields`)。 */ std::string AttributeString(std::uint64_t address, const char* name) const; /** @brief 读取 `char` 数据集为宽字符串(MATLAB 的 char 是 uint16)。 */ bool ReadText(std::uint64_t address, std::wstring& out) const; /** @brief 读取数值数据集为 double 序列(流式转换,不生成整块字节缓冲)。 */ bool ReadNumbers(std::uint64_t address, std::vector& out) const; /** * @brief 读取 Simulink SDI 的字节流数据集(`#data#`)为 double 序列。 * * SDI 的 `#data#` 是 `uint8` 数据集,长度 = 采样数 × 8:按 8 字节一组重新解释为 double。 * 实现上逐 chunk 解压并直接写入输出缓冲(不产生整块字节数组,也不二次拷贝), * 因此 GB 级文件也不会出现“文件 + 未压缩字节 + double”三份内存叠加。 */ bool ReadDoubleStream(std::uint64_t address, std::vector& out) const; /** @brief 读取引用数据集(HDF5 对象引用 = 对象头地址,相对基地址)。 */ bool ReadReferences(std::uint64_t address, std::vector& out) const; private: struct Message { std::uint16_t type = 0; std::vector data; }; /** * @brief 读取一个小端标量(1/2/4/8 字节)。 * * ⚠️ **不能**用 `U8` 逐字节拼:`Cached` 只服务「完全落在同一个 64KB 块内」的请求, * 跨块时返回 nullptr,逐字节拼接就会把缺失的字节当成 0(`U32` 整段为 0、`U64` 高 32 位为 0), * 于是 B-tree 的 chunk 偏移 / 子节点地址被静默读错。这里统一走 `ScalarLE`: * 命中缓存就用缓存,否则退回一次 `ReadAt`。 */ std::uint64_t ScalarLE(std::uint64_t offset, std::size_t bytes) const; std::uint8_t U8(std::uint64_t offset) const; std::uint16_t U16(std::uint64_t offset) const; std::uint32_t U32(std::uint64_t offset) const; std::uint64_t U64(std::uint64_t offset) const; std::uint64_t Addr(std::uint64_t offset) const; std::uint64_t Resolve(std::uint64_t address) const; /** @brief 随机读取一段字节(不经过缓存;累加已读字节数)。 */ bool ReadAt(std::uint64_t offset, void* dst, std::size_t bytes) const; /** * @brief 取一段**元数据**的只读指针(经 64KB 块缓存)。 * * ⚠️ 返回的指针只在下次调用前有效(缓存块可能被替换),调用方必须立即用完。 * @return 成功返回指针;越界或读失败返回 nullptr。 */ const std::uint8_t* Cached(std::uint64_t offset, std::size_t bytes) const; /** @brief 判断 `offset` 处的 4 字节块标识是否等于 `id`(`id` 为 4 字符)。 */ bool Tag(std::uint64_t offset, const char* id) const; /** * @brief 读取一个 NUL 结尾的短字符串(本地堆里的成员名)。 * * 名字很短但可能跨越 64KB 缓存块边界,因此按块边界截断、必要时继续向后读。 */ void ReadCString(std::uint64_t offset, std::string& out) const; /** @brief 丢失顺序预读缓冲(随机跳转后调用)。 */ void DropReadAhead() const; /** * @brief 从文件读一段**数据**(带顺序预读,供小 chunk 连续读取)。 * * 若请求落在当前预读窗口内直接返回指针;否则从 `offset` 起预读最多 4MB。 * @return 成功返回指针(仅在下次调用前有效),否则 nullptr。 */ const std::uint8_t* ReadData(std::uint64_t offset, std::size_t bytes) const; void ReadMessages(std::uint64_t address, std::vector& messages) const; bool ParseType(const std::uint8_t* data, std::size_t size, std::size_t offset, Type& out, std::size_t& consumed) const; void WalkBtree(std::uint64_t node, std::uint64_t heapData, std::vector>& out) const; void WalkSymbolNode(std::uint64_t node, std::uint64_t heapData, std::vector>& out) const; /** @brief 元数据缓存槽(固定槽数,最近最少使用优先淘汰)。 */ struct CacheSlot { std::uint64_t base = 0; std::uint32_t length = 0; std::uint64_t stamp = 0; std::vector bytes; }; void* m_fileHandle = nullptr; /**< `HANDLE`(避免头文件引入 windows.h)。 */ std::uint64_t m_size = 0; /**< 文件字节数。 */ mutable std::uint64_t m_bytesRead = 0; /**< 已实际读取的字节数(进度上报用)。 */ mutable std::vector m_cache; /**< 元数据块缓存。 */ mutable std::uint64_t m_stamp = 0; /**< 缓存访问计数。 */ mutable std::size_t m_lastSlot = 0; /**< 上次命中的槽(连续标量读取的快路径)。 */ mutable std::vector m_readAhead; /**< 顺序预读缓冲。 */ mutable std::uint64_t m_readAheadBase = 0; mutable std::uint32_t m_readAheadLength = 0; mutable bool m_readAheadValid = false; mutable std::vector m_slice; /**< 连续布局切片缓冲(复用容量)。 */ mutable std::vector m_chunk; /**< 大于预读窗口的分块缓冲(复用容量)。 */ mutable ReadProgress m_readProgress; /**< 流式解码进度回调(可空)。 */ mutable bool m_readCancelled = false; /**< 进度回调请求取消。 */ std::uint64_t m_base = 0; std::uint64_t m_root = 0; int m_offsetSize = 8; int m_lengthSize = 8; mutable std::vector> m_heaps; /**< 堆地址 → 数据地址。 */ }; } // namespace jdi #endif // __MAT_H5_HPP__