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 Julia: implementation notes

These notes describe how the Julia implementation is organized. They complement docs/porting-guide.md (the C semantics every implementation must reproduce). Julia compiles to native code and has fixed-width integer types that wrap like C’s, so the closest models are the C++ (cpp/) and Rust (rust/) implementations; 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

julia/ is a Julia package named WFDBX (module WFDBX):

Path Contents
Project.toml package metadata (name, uuid, version, compat)
src/WFDBX.jl the module: includes (in dependency order), core exports, __init__
src/cstd.jl C standard library emulation (glibc-exact printf, strtol, strtod, sscanf, strtok, glibc rand, the standard streams, cexit)
src/types.jl abstract type Wfdb, the types, constants and structures of wfdb.h
src/ecgcodes.jl annotation codes and the <ecgmap.h> functions (state: EcgMaps)
src/wfdbio.jl port of lib/wfdbio.c (state: IoState) and WfdbFile
src/signal.jl, src/annot.jl, src/calib.jl, src/wfdbinit.jl, src/flac.jl the other library files (states SigState, AnnState, CalState)
src/wfdb.jl WfdbLib, the concrete Wfdb (assembles the states), the default instance lib()
src/highlevel.jl high-level API (read_header, read_record, read_annotations, write_record, write_record_physical, write_annotations)
src/check.jl check_record: checks a record and its annotation files and reports every problem
src/cli.jl the driver (main_cli), run_tool, tool_function, tools()
src/apps/<tool>.jl the command-line tools: <tool>_main(args) and the tool’s precompilation workload; every .jl file in src/apps/ is included automatically
src/precompile.jl the precompilation workload registry and helpers
bin/wfdbx.jl command-line driver: julia --startup-file=no --project=julia julia/bin/wfdbx.jl TOOL ARGS...
test/runtests.jl unit tests (Test standard library)

Each file starts with a comment saying what it is, then that it is part of WFDB-x, an independent reimplementation of the WFDB Software Package in Python, C++, MATLAB/Octave, Rust, Julia, R and Kotlin. A file that translates a C file says so (“It is a Julia translation of lib/.c from the WFDB Software Package 10.7.0, by ..."), lists its "Changes from the original" (each marked `wfdb-x:` in the code; `docs/DIVERGENCES.md` gives the reasons), and gives the copyright of the original C code, as in `docs/AUTHORS.md`, and of the Julia translation (Reza Sameni, 2026). Code with no C original says "It has no counterpart in the original package." Every header ends with the license: GNU GPL 3 or later (`LICENSE`).

The docstring of a WFDB library function ends with “Translation of the C function NAME() (lib/FILE.c, WFDB 10.7.0): same arguments and results.” (plus “Differences: …; see docs/DIVERGENCES.md.” where WFDB-x differs); that of a WFDB-x addition ends with “Part of WFDB-x; not in the original WFDB package.”

Data representation

Library state and the API pattern

The state of the C library is held by an object of the abstract type Wfdb; its one concrete type, WfdbLib (wfdb.jl), has one field per C source file, whose type is defined in the file that ports it:

field type defined in
io IoState wfdbio.jl (WFDB path, errors, current record name)
sig SigState signal.jl
ann AnnState annot.jl
cal CalState calib.jl
maps EcgMaps ecgcodes.jl (the <ecgmap.h> tables)

Each state is a mutable struct whose zero-argument constructor gives the state at program start. Add fields to the state of the module being changed only; read another module’s state through its functions where possible.

Every C library function (including library-private ones such as wfdb_open, wfdb_checkname, wfdb_setirec, useful to tools) is a function with the C name taking the library state as its first argument annotated w::Wfdb (the abstract type: the methods are compiled for WfdbLib), plus a method without it that uses the default instance:

"""
    sampfreq([w], record) -> Float64

C `sampfreq(record)`: ...
"""
function sampfreq(w::Wfdb, record::OptByteStr)
    ...
    wfdb_error(w, "sampfreq: can't read header for record %s\n", record)
    ...
end
sampfreq(record::OptByteStr) = sampfreq(lib(), record)

wfdbquit does not reset the counter frequency, setafreq’s resolution, the buffer sizes, the mnemonic tables, the EcgMaps (see docs/KNOWN_C_BEHAVIORS.md).

C standard library emulation (cstd.jl)

Files: WfdbFile

WfdbFile is C’s FILE * / WFDB_FILE (buffered, sticky end-of-file and error flags as glibc). Functions (C names; the wfdb_ aliases of lib/*.c, wfdb_getc, wfdb_fclose, …, also exist): getc (inlined; EOF = -1), ungetc, fread!(f, buf[, n]) (bytes) and fread!(f, buf, size, nitems) (C fread), getline!(f, line), fgets!(f, s, size), read_to_end, wfdb_g16, wfdb_g32, putc (inlined), fwrite(f, bytes), cfprintf, wfdb_p16, wfdb_p32, fflush, ftell, fseek(f, offset, SEEK_SET|SEEK_CUR|SEEK_END), feof, ferror, clearerr, fclose (C’s status), is_std, set_bufsize!. Opened with cfopen, wfdb_fopen, wfdb_open, wfdb_stdin(), wfdb_stdout(), wfdb_stderr(), wfdb_memfile(bytes). Writable files are registered and flushed at exit; code must close the files it opens.

Tools and the command line

Each tool is src/apps/<tool>.jl, defining <tool>_main(args::Vector{String}) (args without the program name), a line-by-line port of the C program (app/<tool>.c or convert/<tool>.c), with the same messages, exit statuses and files. Conventions (see src/apps/wfdbwhich.jl, src/apps/wfdbcat.jl):

function rdsamp_main(args::Vector{String})
    argv = cargs(args)          # C argv[i] (i >= 1) is argv[i]
    argc = length(argv) + 1     # C argc
    pname = "rdsamp"            # messages use the tool's name
    ...
    cexit(status)               # or return the status
end

bin/wfdbx.jl loads the package (precompiled), runs main_cli(ARGS) and exits with its status; an unknown tool gives a message and status 1.

Julia finds its package depot, which holds the compiled package, through HOME. When HOME was changed (to give ~ names another meaning, as the conformance suite does), bin/wfdbx.jl keeps using the depot of the user’s own home directory (from the password database), so that the package is not compiled again. JULIA_DEPOT_PATH, if set, is used as given.

Remote files are read with the Downloads standard library, which is loaded only when the first URL is opened. Each URL is requested at most once per Wfdb instance.

Startup time

Every conformance case starts a new Julia process, so the package must load from its precompiled image without compiling anything at run time. src/precompile.jl runs the registered workloads during precompilation (guarded by ccall(:jl_generating_output, Cint, ()) == 1), so that the native code they exercise is cached (Julia ≥ 1.9 package images). Each library file or tool registers its workload where it is defined:

register_workload("rdsamp") do
    with_workload_data() do dir             # temp copy of tests/data, cwd
        workload_tool("rdsamp", "-r", "100s", "-t", "1")   # run_tool, WFDB=.
        workload_tool("rdsamp", "-r", "100s", "-p", "-v", "-t", "0.5")
    end
end

Cover each option and code path (signal formats, annotation paths, error paths), keep the workloads short, and avoid type instabilities (they force run-time compilation of the unforeseen cases: code reached by a run-time dispatch is not saved in the package image even if the workload ran it). Two common causes:

Check that a run compiles nothing:

cd tests/data && julia --startup-file=no --trace-compile=stderr \
    --project=../../julia ../../julia/bin/wfdbx.jl rdsamp -r 100s -t 1 >/dev/null

prints every method compiled at run time (there should be none). docs/BENCHMARKS.md gives the start-up time of the tools.

getvecs(v, n) (a WFDB-x addition) reads n sample vectors at once; read_record uses it, a block of frames at a time, copying each block into the samples matrix (one column per signal, allocated once for the length of the record). Its bulk path (_bulk_frames! in signal.jl) decodes whole frames straight from the input buffers, for every record without deskewing or resampling whose signals are in the formats 8, 16, 24, 32, 61, 80, 160, 212, 310 or 311 (or null signals): records of several sampling frequencies (in low- and high-resolution modes) and the segments of multi-segment records (mapped to the layout signals when the layout varies) included. It decodes each signal group a tile of frames at a time (_BULK_TILE) into a work space (_bulk_decode!, one loop per format), then stores each signal’s samples (_bulk_signal!: invalid samples, GVPAD, format 8 differences, checksums). The frames it cannot take (at a buffer boundary, at the checksum frame, at a segment boundary, in the middle of a frame in high-resolution mode) go through getvec; the results and the library state are identical. The tests turn it off with WFDBX._BULK_ENABLED[] = false and count the vectors it read with WFDBX._BULK_VECTORS[].

write_record writes its frames with _putframes! (in signal.jl), which encodes the samples straight into the output buffers when every signal is in format 16, 61, 80, 160, 24 or 32 in a local file; the first frame, the frames that fill a buffer and every frame of the other formats go through putvec, so that the files, checksums and library state are identical (WFDBX._BULK_WRITE[] = false turns it off). rdsamp formats its lines in raw units into a preallocated buffer (_rdsamp_put_integer!).

Changing any source file invalidates the package image: the next process precompiles again (with the workloads; parallel processes wait for it).

Testing

julia --project=julia -e 'using Pkg; Pkg.test()'            # unit tests
python3 tests/conformance/harness.py check --impl julia      # conformance
python3 tests/conformance/harness.py check --impl julia rdsamp -v   # one tool