WFDB-x

A fast, lightweight implementation of the WFDB library and command-line tools, with identical behavior across seven programming languages

View the Project on GitHub alphanumericslab/wfdb-x

WFDB-x for Rust: implementation notes

These notes describe how the Rust implementation is organized. They complement docs/porting-guide.md (the C semantics every implementation must reproduce). The C++ implementation (cpp/) is the closest model (a statically typed, compiled port with the same integer types); the Python implementation (python/src/wfdbx/) shows how each undefined C behavior was resolved. All implementations must behave identically (the conformance suite checks this).

Requirements

Layout

One crate, wfdbx (rust/Cargo.toml):

Path Contents
src/lib.rs crate documentation; module list; glob re-exports (pub use module::*) of the library modules, so a new public function needs no change to lib.rs
src/wfdb.rs the Wfdb struct, the default instance (with_default), OptBytes/NULL
src/types.rs types, constants (WFDB_*, DEFWFDB, …) and structures (Siginfo, Calinfo, Anninfo, Annotation, Seginfo) of wfdb.h
src/cstd.rs, src/cstd/ C standard library emulation: printf formatting (glibc-exact; printf.rs, float.rs), strtol, strtod, atoi, atof, sscanf, strtok, fgets line splitting (parse.rs), glibc rand (rand.rs), standard streams, exit, argv, environment (stdio.rs)
src/wfdbio.rs port of lib/wfdbio.c: finding files (input/output directories, network access via curl, the WFDB path of legacy mode), wfdb_open, errors, WfdbFile
src/signal.rs port of lib/signal.c
src/annot.rs port of lib/annot.c (and the annotation code mnemonics)
src/calib.rs port of lib/calib.c
src/wfdbinit.rs port of lib/wfdbinit.c
src/flac.rs FLAC decoder and encoder (port of cpp/src/flac.cpp; crate-private)
src/ecgcodes.rs annotation codes (NORMAL, …) and the ecgmap.h functions
src/record.rs high-level API (read_record, read_header, read_annotations, write_record, write_record_physical, write_annotations)
src/check.rs check_record (a WFDB-x addition; python/src/wfdbx/check.py is the reference)
examples/check_cases.rs prints the check_record reports of a corpus made by python/tests/check_corpus.py (its expected.txt format)
src/bin/<tool>.rs the command-line tools, one binary each
tests/ integration tests (cargo test)

cargo build --release puts the tools in rust/target/release/<tool>, where the conformance harness finds them (--impl rust).

The library object and the C-style API

All the state of the C library (open signals and annotators, WFDB path, time base, …) is held by one struct, Wfdb. Each C library function is a method of Wfdb with the C name (w.isigopen(...), w.getvec(...)). Internal functions are methods too, so they call each other through &mut self (no global borrow is needed).

For line-by-line ports of C programs, every method is also available as a free function with the same name and arguments (wfdbx::isigopen(...)), operating on a default instance held in a thread_local! RefCell<Wfdb>. The free functions are thin wrappers (with_default(|w| w.isigopen(...))); they must not be called from inside a method (that would borrow the default instance twice).

Several independent Wfdb instances can be used at the same time (each has its own state, unlike the C library); a Wfdb is Send.

The state of each C source file is a sub-struct of Wfdb, defined in the module that ports it, so that the modules can be written independently:

field type defined in
io IoState wfdbio.rs
sig SigState signal.rs
ann AnnState annot.rs
cal CalState calib.rs
maps EcgMaps ecgcodes.rs (the modifiable <ecgmap.h> tables)

Each state implements Default (the state at program start). Its fields are private to its module (or pub(crate) where another module needs them); other modules use the methods. A state must not implement Drop to do anything but flush buffers: dropping a Wfdb (which cstd::exit does for the default instance) must behave like C’s exit, which flushes open streams but does not close records (no annotation end marks, no header updates).

Methods that are library-private in C (wfdb_open, wfdb_checkname, wfdb_setirec, …) are pub methods too (they are useful to tools, e.g. wfdbcat uses wfdb_open); hooks used only by other modules (wfdb_sigclose, wfdb_anclose, wfdb_osflush, …) are pub(crate).

Writing a library function

impl Wfdb {
    /// C `sampfreq(record)`: ...
    ///
    /// Translation of the C function sampfreq() (lib/signal.c, WFDB 10.7.0):
    /// same arguments and results. Differences: ...; see docs/DIVERGENCES.md.
    pub fn sampfreq(&mut self, record: impl OptBytes) -> f64 { ... }
}

/// C `sampfreq` on the default instance (see [`Wfdb::sampfreq`]).
///
/// Translation of the C function sampfreq() (lib/signal.c, WFDB 10.7.0):
/// same arguments and results. Differences: ...; see docs/DIVERGENCES.md.
pub fn sampfreq(record: impl OptBytes) -> f64 {
    with_default(|w| w.sampfreq(record))
}

The last paragraph of the documentation of a public function says where it comes from: “Translation of the C function NAME() (lib/FILE.c, WFDB 10.7.0): same arguments and results.”, followed by “Differences: …; see docs/DIVERGENCES.md.” when the WFDB-x behavior differs; or, for a function that WFDB-x adds (the high-level API, check_record, getvecs, the wfdbx_* settings, …), “Part of WFDB-x; not in the original WFDB package.”

File headers

Each source file begins with its //! module documentation (its purpose), followed by a // header comment. For a file that translates C code, the header says that the file is part of WFDB-x, names the C file(s) it translates and their authors, lists the changes from the original (“Changes from the original: …”; each is marked wfdb-x: in the code, and docs/DIVERGENCES.md gives the reasons), gives the copyright of the C code (from docs/AUTHORS.md) and of the translation, and the license notice (GPL-3 or later). A file with no C original (the high-level API, check.rs, cstd, flac.rs, the tests) says “It has no counterpart in the original package.” instead. Copy the header of a similar file when adding one.

Data representation

Output and errors

Files: WfdbFile

WfdbFile is C’s FILE * / WFDB_FILE: WfdbFile::fopen(name, mode) (plain fopen), wfdb_fopen (the library’s checks), WfdbFile::stdin(), stdout(), stderr(), from_bytes(Vec<u8>) (in memory). Methods: getc (inlined; EOF = -1), ungetc, read, fread, getline, fgets, read_to_end, g16, g32, putc (inlined), write, printf (and cfprintf!), p16, p32, flush, tell, seek(offset, SEEK_SET|SEEK_CUR|SEEK_END), eof, error, clearerr, close (C’s fclose status), is_std. End of file is sticky (as in glibc >= 2.28) until clearerr or a seek. Dropping a WfdbFile flushes it. A file on the standard input never closes it, and gives back the bytes it buffered but did not consume to the next reader of the standard input. wfdbio::freopen_stdin(name) is C’s freopen(name, "rb", stdin): later readers of the standard input (WfdbFile::stdin(), wfdb_open of -) read that file instead (used by a2m and ad2m).

Tools

Each tool is src/bin/<tool>.rs, a line-by-line port of the C program (app/<tool>.c or convert/<tool>.c), with the same messages, exit statuses and files. argv is taken with std::env::args_os() and kept as bytes (cstd::args()). Messages that include the program name use the tool’s name (argv[0]’s last component, as in C).

oannclose sorts out-of-order annotation files in the same process (sort_ann_file in annot.rs, with the list of sortann, annot::AnnList, which the sortann tool uses too), with private reader, writer and definition table objects. In legacy mode it runs the sortann tool (from the directory of the running executable, else from PATH), as the C library runs sortann: wfdbio::run_sortann(record, annotator) runs sortann -r. RECORD -a ANNOTATOR (without a shell) after flushing the standard output and error, and returns its exit status.

A tool’s main reads let argv = cstd::args();, writes with cprintf! / ceprintf!, and ends with cstd::exit(status) (see src/bin/wfdbwhich.rs and src/bin/wfdbcat.rs).

Bulk reading in getvecs

getvecs(buf, n) reads n sample vectors with the semantics of n calls of getvec. Wfdb::bulk_frames decodes whole frames at once, straight from the input buffers, where it can: formats 8, 16, 24, 32, 61, 80, 160, 212, 310 and 311 (one format per signal group), and null signals (format 0) of known length, without deskewing or resampling. This covers records of several sampling frequencies, in low-resolution mode (averaging the oversampled signals with meansamp) and in high-resolution mode (from the start of a frame), and the segments of multi-segment records, of fixed or variable layout (mapped with sigmap), null segments included. Only the complete frames already in the input buffers are decoded: a frame that crosses the end of a buffer (whose reading refills it), the frame at which checksums are verified or a null signal ends, the change from one segment to the next and everything the bulk path does not decode go through getvec, so the files are read in the same blocks, and the statuses, messages and state (samples, checksums, counts, gvc) are those of frame-by-frame reading. The frames are decoded in tiles of up to 1024 (BULK_TILE, whole packed units): a group’s raw samples into a small buffer (bulk_decode), then signal by signal into the frames (bulk_signal: invalid samples, the last sample and the checksum, as getskewedframe handles each sample). wfdbx_bulk_read(false) turns it off; tests/bulk.rs compares both paths.

The high-level API (record.rs)

Testing

cd rust && cargo build --release && cargo test --release
cargo fmt && cargo clippy --release --all-targets         # formatting (rustfmt.toml), lints
python3 tests/conformance/harness.py check --impl rust     # from the repository root
python3 tests/conformance/harness.py check --legacy --impl rust   # legacy mode

cargo test also runs the examples of the documentation. Most of them read the sample record 100s (the first minute of record 100 of the MIT-BIH Arrhythmia Database) from tests/data. Every example that names a record says what it is and where it is read from: the visible line sets the input directory to tests/data (for a reader at the root of the repository), inside hidden # /* and # */ lines, and a hidden line before it sets ../tests/data (doctests run in the rust directory). The legacy values of the library functions are checked against the C reference (tests/reference/api_expect.c).

The code must build without warnings (cargo build, cargo clippy).

To build a single tool, use cargo build --release --bin <tool>.