hdf5/doc/getting-started-with-hdf5-development.md
2023-10-03 09:01:28 -07:00

38 KiB

Getting Started with HDF5 Development

Introduction

The purpose of this document is to introduce new HDF5 developers to some of the quirks of our source code. It's not a style guide (see the forthcoming HDF5 style guide for that), but instead describes the most commonly encountered features that are likely to trip up someone who has never worked with the HDF5 source code before.

Corrections and suggestions for improvement should be handled via GitHub pull requests and issues.

Getting started

Building the library for development

You don't really need special configuration settings for building the library as a developer.

Some tips that may be useful:

  • Building in debug mode will turn on additional checks in many packages. You'll probably want to start coding in debug mode.
  • You can turn symbols on independently of debug/production mode.
  • If you will be looking at memory issues via tools like valgrind, you will need to turn off the free lists, which recycle memory so we can avoid calling malloc/free. This is done using the --enable-using-memchecker configure option. Some developers build with this all the time, as the memory recyclilng can hide problems like use-after-free.
  • You can enable developer warnings via --enable-developer-warnings. These warnings generate a lot of noise, but the output can occasionally be useful. I probably wouldn't turn them on all the time, though, as they can make it harder to spot warnings that we care about.
  • You can set warnings as errors. We have an older scheme that does this for a subset of errors or you can simply specify -Werror, etc. as a part of CFLAGS. Configure is smart enough to strip it out when it runs configure checks. We build the C library with -Werror on GitHub, so you'll need to fix your warnings before creating a pull request.
  • CMake has a developer mode that turns most these settings on.

Branches

Please see doc/branches-explained.md for an explanation of our branching strategy.

For new small features, we have developers create feature branches in their own repositories and then create pull requests into the canonical HDF5 repository. For larger work, especially when the work will be done by multiple people, we create feature branches named feature/<feature>. If work stops on a feature branch, we rename it to inactive/<feature>.

If you create a feature branch in the canonical HDFGroup repository, please create a BRANCH.md text file in the repository root and explain:

  • The branch's purpose
  • Contact info for someone who can tell us about the branch
  • Clues about when the branch will be merged or can be considered for retirement

The purpose of this document is to avoid orphan branches with no clear provenance.

Pull requests

The process of creating a pull request is explained in CONTRIBUTING.md.

A brief tour of the source code

Here's a quick guide to where you can find things in our source tree. Some of these directories have README.md files of their own.

bin/ Scripts we use for building the software and misc. tools.

c++/ Source, tests, and examples for the C++ language wrapper.

config/ Configuration files for both the Autotools and CMake.

doc/ Miscellaneous documents, mostly in markdown format.

doxygen/ Mainly Doxygen build files and other top-level Doxygen things. The Doxygen content is spread across the library's header files but some content can be found here when it has no other obvious home.

examples/ C library examples. Fortran and C++ examples are located in their corresponding wrapper directory.

fortran/ Source, tests, and examples for the Fortran language wrapper.

hl/ Source, tests, and examples for the high-level library.

java/ Source, tests, and examples for the JNI language wrapper and the corresponding OO Java library.

m4/ m4 build scripts used by the Autotools. CMake ignores these.

release_docs/ Install instructions and release notes.

src/ Source code for the C library.

test/ C library test code. Described in much more detail below.

testpar/ Parallel C library test code. Described in much more detail below.

tools/ HDF5 command-line tools code, associated tools tests, and the input test files.

utils/ Small utility programs that don't belong anywhere else.

General Things

Necessary software

In order to do development work on the HDF5 library, you will need to have a few things available.

  • A C99-compatible C compiler (MSVC counts). C11 is required to build the subfiling feature.
  • Either CMake or the Autotools (Autoconf, Automake, libtool)
  • Perl is needed to run some scripts, even on Windows
  • A C++11-compatible compiler if you want to build the C++ wrappers
  • A Fortran 2003-compatible compiler if you want to build the Fortran wrappers
  • A Java 8-compatible compiler if you want to build the Java wrappers
  • flex/lex and bison/yacc if you want to modify the high-level parsers
  • A development version of zlib is necessary for zlib compression
  • A development version of szip is necessary for szip compression
  • An MPI-3 compatible MPI library must be installed for parallel HDF5 development
  • clang-format is handy for formatting your code before submission to GitHub. The formatter will automatically update your PR if it's mis-formatted, though, so this isn't strictly necessary.
  • codespell is useful to identify spelling issues before submission to GitHub. The codespell action won't automatically correct your code, but it will point out spelling errors, so this also isn't strictly necessary.

These are the requirements for working on the develop branch. Maintenance branches may relax the required versions somewhat. For example, HDF5 1.12 and earlier only require C++98.

Certain optional features may require additional libraries to be installed. You'll need curl and some S3 components installed to build the read-only S3 VFD, for example.

Platform-independence

HDF5 assumes you have a C99 compiler and, to a certain extent, a POSIX-like environment (other languages will be discussed later). On most operating systems in common use, this will be a reasonable assumption. The biggest exception to this has been Windows, which, until recently, had poor C99 compliance and spotty POSIX functionality. To work around differences in platforms and compilers, we've implemented a compatibility scheme.

Unlike most codebases, which test for features and inject their own normal-looking functions when there are deficiencies, HDF5 uses a scheme where we prefix all C and POSIX calls with HD (e.g., HDmalloc). The H5private.h header handles most of the fixup for Unix-like operating systems and defines the HD replacements. For Windows, we first parse the H5win32defs.h file, which maps missing Windows and MSVC functionality to POSIX and C99 calls. H5private.h tests for previously defined HD calls and skips redefining it if it already exists. H5system.c includes Windows glue code as well as a few functions we need to paper over differences in Unix-like systems.

One thing to keep in mind when looking at our platform-independence layer is that it is quite old, having been authored when the Unix world was much more fragmented and C99 was uncommon. We've slowly been reworking it as POSIX has standardized and C99 become widespread.

Another thing to keep in mind is that we're fairly conservative about deprecating support for older platforms and compilers. There's an awful lot of ancient hardware that requires HDF5, so we try to only make major changes to things like language requirements when we increment the major version (minor version prior to HDF5 2.0).

C99

We assume you have a C99 compiler. Subfiling uses some C11 features, but that is compiled on demand and we are not moving that requirement to the rest of the library. All modern compilers we've tested can handle HDF5's C99 requirements, even Microsoft's. In the future, we'll remove the HD prefixes from all standard C library calls.

One quirk of HDF5's age, is the hbool_t type, which was created before C99 Booleans were widespread and which uses TRUE and FALSE macros for its values instead of C99's true and false. We plan to switch this over to C99's Boolean types sometime in the near future.

POSIX

We assume basic POSIX.1-2008 functionality is present. When a POSIX (or common Unix) function is missing on a popular platform, we implement a shim or macro in H5private.h and/or H5system.c. Systems that need a lot of help, like Windows, have gotten special headers in the past (e.g., H5win32defs.h) but now that most platforms implement the POSIX and C99 functionality we need, these special headers are less necessary.

Threads

Thread-safety was originally implemented using Pthreads, with Win32 support bolted on later. No other thread libraries are supported. The subfiling feature uses multiple threads under the hood, but that's out of scope for an introductory document. Thread-related code is largely confined to the H5TS files, where we define HDF5-specific primitives and then map Pthread or Win32 implementations onto them.

C++

The C++ Wrappers require C++11. We generally only require the rule of three for the classes.

Fortran

The Fortran wrappers require Fortran 2003.

Java

The Java wrappers require Java 8.

Warning suppression

In the rare cases where we've decided to suppress a warning, we have a set of function-like macros that we use for that. They are located in H5private.h and have the form H5_<compiler>_DIAG_(OFF|ON) and take the name of the warning they are suppressing as a parameter. They were originally designed for gcc and extended to clang. They have not been updated for other compilers. Instead, we have plans to revamp the macro system to be more generic and extensible.

We try to configure the compilers we use the most for maximum grumpiness and then fix all the warnings we can. Please don't use the warning suppression macros in lieu of actually fixing the underlying problems.

Build Systems

We support building the library with both the Autotools and CMake. We'd like to eventually move to only having one build system, which would be CMake since the Autotools don't really support Windows, but that seems unlikely to happen anytime soon. With few exceptions, any new feature, test, or configure option should be supported in both build systems.

The particulars of the build systems can be found in the config directory and its subdirectories.

Working in the library

Anatomy of an HDF5 API call

HDF5 API calls have a uniform structure imposed by our function enter/leave and error handling schemes. We currently stick to this boilerplate for ALL functions, though this may change in the future. The general boilerplate varies slightly between internal and public API calls.

Here's an example of an internal API call:

/*
 * Function comments of dubious value
 */
herr_t
H5X_do_stuff(/*parameters*/)
{
	/* variables go here */
	void *foo = NULL;
	herr_t ret_value = SUCCEED;

	FUNC_ENTER_NOAPI(FAIL)

	HDassert(/*parameter check*/);

	if (H5X_other_call() < 0)
		HGOTO_ERROR(H5E_MAJ, H5E_MIN, FAIL, "badness");

done:
	if (ret_value < 0)
		/* do error cleanup */
	/* do regular cleanup stuff */

	FUNC_LEAVE_NOAPI(ret_value);
}

There are a couple of things to note here.

  • Each function call has a header comment block. The information you'll find in most function comments is not particularly helpful. We're going to improve the format of this.
  • Most functions will return herr_t or hid_t ID. We try to avoid other return types and instead use out parameters to return things to the user.
  • The name will be of the form H5X_do_something() with one or two underscores after the H5X. The naming scheme will be explained later.
  • Even though C99 allows declarations anywhere in the function, we put most of them at the top of the file, with the exception of loop variables and variables that are "in scope" inside an ifdef.
  • We generally initialize values that may need to be freed or otherwise cleaned up to a "bad" value like NULL or H5I_INVALID_HID so we can better clean up resources on function exit, especially when there have been errors.
  • Most non-void functions will declare a variable called ret_value. This is used by the error handling macros. It's usually the last thing declared in the variable block.
  • Every function starts with a FUNC_ENTER macro, discussed later in this document.
  • Most internal calls will check parameters via asserts.
  • We check the return value of any call that can return an error, using the form shown.
  • On errors, an error macro is invoked. These are described later in this document.
  • Any function that returns an error will have a done target. Most error macros jump to this location on errors.
  • We do most cleanup at the end of the function, after the done target. There are special DONE flavors of error macro that we use in post-done cleanup code to detect and report errors without loops.
  • Almost every function ends with a FUNC_LEAVE macro.

And here's an example of a public API call:

/*
 * Doxygen stuff goes here
 */
herr_t
H5Xdo_api_stuff(/*parameters*/)
{
	/* variables go here */
	herr_t ret_value = SUCCEED;

	FUNC_ENTER_API(FAIL)
	H5TRACE3(/*stuff*/)

	if (/*parameter check*/)
		HGOTO_ERROR(H5E_ARGS, H5E_BADVALUE, FAIL, "badness");

	/* VOL setup */

	if (H5VL_call() < 0)
		HGOTO_ERROR(H5E_FOO, H5E_BAR, FAIL, "badness");

done:
	if (ret_value < 0)
		/* do error cleanup */
	/* do regular cleanup stuff */

	FUNC_LEAVE_API(ret_value);
}

A public API call differs little from an internal call. The biggest differences:

  • Public API calls are commented using Doxygen. This is how we generate the reference manual entries.
  • The name will have the form H5Xdo_stuff(), with no underscores after the H5X.
  • The function enter macro is FUNC_ENTER_API (or similar). Under the hood, this one differs quite a bit from an internal function enter macro. It checks for package initialization, for example, and acquires the global lock in thread-safe HDF5.
  • There is a TRACE macro. This helps with API tracing and is applied by a script invoked by autogen.sh (Autotools) or CMake. You probably don't need to worry much about this.
  • Parameter checking uses the regular HDF5 error scheme and invokes HGOTO_ERROR macros on errors.
  • In storage-related calls, there will usually be some VOL setup (HDF5 1.12.x and later) and in lieu of a regular internal API call, there will be an H5VL VOL call.
  • The function exit macro will be FUNC_LEAVE_API (or similar). This is where we release the global thread-safe lock, etc.

Public, private, package

HDF5 is divided into packages, which encapsulate related functionality. Each has a prefix of the form H5X(Y). An example is the dataset package, which has the prefix H5D. Hopefully, you are familiar with this from the public API. In addition to the public packages, we all know and love, there are many internal packages that are not visible to the public via API calls, like H5FL (free lists / memory pools) and H5B2 (version 2 B-trees). There's also an H5 package that deals with very low-level things like library startup.

API calls, types, etc. in HDF5 have three levels of visibility. From most to least visible, these are:

  • Public
  • Private
  • Package

Public things are in the public API. They are usually found in H5Xpublic.h header files. API calls are of the form H5Xfoo(), with no underscores between the package name and the rest of the function name.

Private things are for use across the HDF5 library, and can be used outside the packages that contain them. They collectively make up the "internal library API". API calls are of the form H5X_foo() with one underscore between the package name and the rest of the function name.

Package things are for use inside the package and the compiler will complain if you include them outside of the package they belong to. They collectively make up the "internal package API". API calls are of the form H5X__foo() with two underscores between the package name and the rest of the function name. The concept of "friend" packages exists and you can declare this by defining <package>_FRIEND in a file. This will let you include the package header from a package in a file that it is not a member of. Doing this is strongly discouraged, though. Test functions are often declared in package headers as they expose package internals and test programs can include multiple package headers so they can check on internal package state.

Note that the underscore scheme is primarily for API calls and does not extend to things like types and symbols. Another thing to keep in mind is that the difference between package and private API calls can be somewhat arbitrary. We're hoping to improve the coherence of the internal APIs via refactoring.

Function enter and leave macros

Function enter and leave macros are added to almost all HDF5 API calls. This is where we set up error handling (see below) and things like the thread-safety global lock (in public API calls). There are different flavors depending on the API call and it's very important that they are appropriate for the function they mark up.

The various combinations you are most likely to encounter:

Macro Use
FUNC_ENTER_API Used when entering a public API call
FUNC_ENTER_NOAPI Used when entering a private API call
FUNC_ENTER_PACKAGE Used when entering a package API call

There are also _NO_INIT flavors of some of these macros. These are usually small utility functions that don't initialize the library, like H5is_library_threadsafe(). They are uncommon.

You may also come across _NO_FS ("no function stack") flavors that don't push themselves on the stack. These are rare.

For the most part, you will be using the API, NOAPI, and PACKAGE macros.

You may see other versions if you are working in a maintenance branch, like the STATIC macros that we removed in 1.13. We've been working to reduce the complexity and number of these macros and we don't always push that downstream due to the scope of the changes involved. You should be able to figure out what any new macros do based on what you've seen here, though.

Error macros

Almost all HDF5 functions return an error code, usually -1 or some typedef'd equivalent. Functions that return void should be avoided, even if the function cannot fail. Instead, return an herr_t value and always return SUCCEED.

Type Error Value
herr_t FAIL
any signed integer type -1
hid_t H5I_INVALID_HID
htri_t FAIL
haddr_t HADDR_UNDEF
pointer NULL

We've been trying to move away from using anything other than herr_t or hid_t to return errors, as eliminating half of a variable's potential values just so we can return a 'bad' value on errors seems unwise in a library that is designed to scale.

herr_t is a typedef'd signed integer. In the library, we only define two values for it: SUCCEED and FAIL, which are defined to 0 and -1, respectively, in H5private.h. We do not export these values, so public API calls just note that herr_t values will be negative on failure and non-negative on success.

Most of the error handling is performed using macros. The only extra setup you will have to do is:

  1. Create a variable named ret_value with the same type as the return value for the function. If the type is herr_t it is frequently set to SUCCEED and will be set to FAIL on errors. In most other cases, the value is initialized to the 'bad' value and the function's code will set ret_value to a 'good' value at some point, with errors setting it back to the 'bad' value.

  2. Create a done target (done:) just before you start your error cleanup. This will be the point to which the error macros will jump.

We check for errors on almost all internal lines of C code that could putatively fail. The general format is this:

if (function_that_could_fail(foo, bar) < 0)
    HGOTO_ERROR(H5E_<major>, H5E_<minor>, <bad value>, "tell me about badness");

HGOTO_ERROR is one of a set of macros defined in H5Eprivate.h. This macro pops an error on the error stack and sets the return value to <bad value>, then jumps to the done: target.

Major and minor codes are a frequent cause of confusion. A full list of them can be found in H5err.txt, which is processed into the actual error header files at configure time by the bin/make_err script. The original intent was for major and minor error codes to be strongly associated. i.e., a given minor code would only be used with its associated major code. Unfortunately, this has not been the case in practice, and the emitted text can appear nonsensical in error stack dumps. Even worse, the major and minor error codes are used inconsistently throughout the library, making interpreting them almost impossible for external users. We hope to address this deficiency in the near future.

In the meantime, the following guidelines can be helpful:

  1. Use H5E_ARGS as the major error category when parsing function parameters. The minor code will usually be H5E_BADVALUE, H5E_BADRANGE, or H5E_BADTYPE.
  2. Otherwise use the same major code throughout the source file. There is almost a 1-1 correspondence between packages and major error codes.
  3. Pick the minor code that seems to match the API call. You can grep through the library to find similar uses.
  4. The string at the end of the HGOTO_ERROR macro is much more important, so make sure that is helpful

You will still sometimes see the major error code match the package of a failing function call. We're trying to fix those as we come across them.

Since the HGOTO_ERROR macro jumps to the done: target, you can't use it after the done: target without creating a loop. Instead, you'll need to use the HDONE_ERROR macro, which will handle errors without jumping to the target. Instead, processing will continue after pushing the error and setting the return value, in the hopes that we can clean up as much as possible.

At the end of the function, the FUNC_LEAVE macro will return ret_value.

Trace macros

These are automatically generated for public C library API calls by the bin/trace script, which scans the source code, looking for functions of the form H5X(Y?)<whatever>(), to which it will add or update the H5TRACE macros.

H5TRACE macros are only added to public C library API calls. They are NOT added to the language wrappers, tools code, high-level library, etc.

You should never have to modify an H5TRACE macro. Either point bin/trace at your source file or run autogen.sh (which runs bin/trace over the C files in src). bin/trace is a Perl script, so you'll need to have that available.

Memory - H5MM and H5FL

In the C library itself, we use H5MM and H5FL calls to allocate and free memory instead of directly using the standard C library calls.

The H5MM package was originally designed so that it would be easy to swap in a memory allocator of the developer's choosing. In practice, this has rarely been a useful feature, and we are thinking about removing this scheme. In the meantime, almost all memory allocations in the C library will use the H5MM (or H5FL) package.

In the past, we added memory allocation sanity checking to the H5MM calls which added heap canaries to memory allocations and performed sanity checking and gathered statistics. These were turned on by default in debug builds for many years. Unfortunately, there is interplay between library-allocated and externally-allocated memory in the filter pipeline where the heap canaries can easily get corrupted and cause problems. We also have some API calls that return library-allocated memory to the user, which can cause problems if they then use free(3) to free it. Given these problems, we now have the sanity checks turned off by default in all build modes. You can turn them back on via configure/CMake options, but it's normally easier to use external tools like valgrind or the compiler's memory debugging options.

H5FL provides memory pools (Free Lists) that create a set of fixed-size allocations of a certain type that the library will reuse as needed. They use H5MM calls under the hood and can be useful when the library creates and frees a lot of objects of that type. It's difficult to give a good guideline as to when to use the H5FL calls and when to use the H5MM calls, but it's probably best to lean towards H5MM unless you can identify a clear performance hit due to memory cycling. Current library usage can be a good guide, but keep in mind that the free lists are probably overused in the library. Another thing to keep in mind is that the free lists can hide memory errors, like use-after-free. Some developers always turn them off and you'll need to turn them off when running memory checkers like valgrind.

Using free list calls differs little from using H5MM calls. There are equivalents for malloc(3), calloc(3), and free(3):

C Library H5FL
malloc H5FL_MALLOC
calloc H5FL_CALLOC
free H5FL_FREE

Since free lists provide pre-allocated memory of a fixed size, you can't reallocate free list memory and there's no H5FL realloc(3) equivalent.

You'll also need to add a setup macro to the top of the file. There are a few flavors defined in H5FLprivate.h. Each creates global free list variables, so there are flavors for extern, static, etc.

Macro Purpose
H5FL_DEFINE Define a free list that will be used in several files
H5FL_EXTERN Define a free list that was defined in another file
H5FL_DEFINE_STATIC Define a free list that will only be used in this file

You will also see ARR, BLK, SEQ, and FAC flavors of the macros. Their use is beyond the scope of a guide for beginners.

Testing

Two macro schemes

The HDF5 C library is tested via a collection of small programs in the test/ directory. There are a few schemes in use:

  • testhdf5 - A larger, composite test program composed of several test files, most of which start with 't' (e.g., tcoords.c)
  • Shell/Powershell scripts that test things like SWMR and flush/refresh behavior. These scripts run small sub-programs.
  • Everything else. These are self-contained test programs that are built and run independently by the test harness.

The test programs do not use a standard test framework like cppunit, but instead use HDF5-specific macros to set up the tests and report errors. There are two sets of macros, one in testhdf5.h and another in h5test.h. Originally, the testhdf5 programs used the macros in testhdf5.h and everything else used the macros in h5test.h, but over time the tests have evolved so that all tests usually include both headers.

This is unfortunate, because it's very important to not mix up the "test framework macros" in each scheme. The testhdf5.h macros register errors by setting global variables and normally continue with the test when they encounter errors. The h5test.h macros indicate errors by jumping to an error target and returning a FAIL herr_t value. If you combine these two macro sets, you may accidentally create tests that fail but do not register the failure.

We are aware that our testing scheme needs some work and we'll be working to improve it over the next year or so.

The command-line tools are tested using a different scheme and are discussed elsewhere.

testhdf5.h

The macros in this file are almost exclusively used in the testhdf5 program. They register errors by incrementing a global error count that is inspected at the end of each test (not each test function, but each test - e.g., after test_file() in tfile.c runs, but not when the individual functions it calls run).

Test functions generally look like this:

static void
test_something()
{
    /* Variables go here */
    int out = -1;
    herr_t ret;

    MESSAGE(5, ("Testing a thing\n"));

    ret = H5Xsome_api_call(&out);
    CHECK(ret, FAIL, "H5Xsome_api_call()");
    VERIFY(out, 6, "incorrect value for out");
}

The MESSAGE macro just dumps the name of what we're testing when we've the verbosity cranked up. The confusingly-named CHECK and VERIFY macros are members of a suite of check macros in testhdf5.h. CHECK macros check to see if a variable is NOT a value, VERIFY macros check to see if a variable IS a value. There are different flavors of macro to match different types so be sure to use the right one to avoid compiler warnings and spurious errors. Under the hood, these macros will emit error information and increment the global error variables.

Tests are added to testhdf5 in testhdf5.c using the AddTest() call. Each test will have a driver function, usually named something like test_<thing>() that invokes the test functions in the file. Most tests will cleanup their files using a cleanup_<thing>() call. If you are deleting HDF5 files, you should use H5Fdelete() instead of remove(3) so that files can be cleaned even when they use alternative VFDs or VOL connectors. You'll also need to add prototypes for any new test driver or cleanup functions to testhdf5.h.

Because these macros interact with global variables that are only used in the testhdf5 program, they are useless anywhere else in the library. Even worse, it will look like you are testing functionality, but errors will not be picked up by the non-testhdf5 programs, hiding problems.

h5test.h

These are the most commonly used macros and are used throughout the test code, even in places that are not specifically tests. Unlike the scheme used in the testhdf5 program, these macros work more like the library, jumping to an error: target on errors. There is no common ret_value variable, however.

Test functions will usually look like this:

static herr_t
test_something()
{
    hid_t fapl_id = H5I_INVALID_HID;
    hid_t fid = H5I_INVALID_HID;
    char filename[1024];
    int *buf = NULL;

    TESTING("Testing some feature");

    if ((fapl_id = h5_fileaccess()) < 0)
        TEST_ERROR;
    h5_fixname(FILENAME[0], fapl, filename, sizeof(filename));

    if ((fid = H5Fcreate(filename, H5F_ACC_TRUNC, H5P_DEFAULT, fapl_id)) < 0)
        TEST_ERROR;

    /* Many more calls here */

    PASSED();
    return SUCCEED;

error:

    HDfree(buf);

    H5E_BEGIN_TRY
    {
        H5Fclose(fid);
    }
    H5E_END_TRY

    return FAIL;
}

Tests begin with a TESTING macro that emits some text (unlike the testhdf5 case, this is always dumped). Any errors will be handled by one of the TEST_ERROR macros. For the most part, TEST_ERROR will suffice, but there are others in h5test.h if you want to emit custom text, dump the HDF5 error stack when it would not normally be triggered, etc.

Most tests will be set up to run with arbitrary VFDs. To do this, you set the fapl ID using the h5_fileaccess() function, which will check the HDF5_DRIVER environment variable and set the fapl's VFD accordingly. The h5_fixname() call can then be used to get a VFD-appropriate filename for the H5Fcreate(), etc. call.

In the error section, we clean up resources and return a 'bad' value, which will usually either be FAIL or -1.

The main() function of each test program will usually start out by calling h5_reset(), then run each test, incrementing an error count variable if a test fails. The exit code and text will be set based on the error count.

Scripts

If you need to fire off multiple programs to test a new feature, you may have to do this via a script. These are normally named test_<thing>.sh.in. The .in is because the scripts are often modified and copied during the configure step. In the past, we have tried to stick to POSIX Bourne shell scripts, but many scripts now require bash.

If you write a new test script, it is important to also add a PowerShell equivalent for testing on Windows.

It's helpful to run any new shell scripts through shellcheck (https://www.shellcheck.net/) to ensure that your scripts are free from common problems.

Parallel tests (in testpar/)

To be covered in a future update of this guide...

Adding new tests

All new HDF5 library functionality (including bugfixes) should have a test. Some rules of thumb:

  • If you need to run multiple programs, you'll need to create a script and some test programs. Use the macros in h5test.h to handle errors in your test programs.
  • If a suitable test program already exists (especially if your tests will be small), add your new tests to the existing file.
  • If you need to create a new test program, create one that uses the h5test.h macros.
  • Avoid adding new tests to testhdf5 or using the macros in testhdf5.h.

Don't forget that you'll need to add your test program or script to the lists in both the CMake and Autotools test files (CMakeLists.txt and Makefile.am in test/ respectively). For simple tests, you just need to add your new test to the list of tests.

All new tests MUST run under both the Autotools and CMake. Ideally, they should also work on Windows, but we're willing to overlook this for things that are unlikely to be useful on that platform.

Documentation

We have moved the user guide and reference manual to Doxygen. All public API calls and symbols should have Doxygen markup in the public header file. New major features should be documented in the user guide. This Doxygen content is located in the package's module header file (H5Xmodule.h). Language wrapper calls (C++/Fortran/Java) should also have Doxygen markup, which will be located with the wrapper source code. Images and other common Doxygen files belong in the doxygen directory.

Internal documentation for developer consumption is currently stored as Markdown files in the doc directory. This may change in the future. Documentation that helps understand the contents of a directory is often stored in a README.md Markdown file in that directory.

Build and install documentation is stored in text files in release_docs. This is admittedly not the best place for this. History files are also kept here.

Command-Line Tools

The HDF5 command-line tools are written in C and built with the library by default. The code is organized into a central tools library (in the tools/lib directory) that includes some common functionality and the individual programs, each of which has its own directory in tools/src. A few of the smaller tools are aggregated into the tools/src/misc directory. Only h5diff has a parallel version at this time and the parallel-specific functionality is in the tools/src/h5diff/ph5diff_main.c file. Some h5diff functionality has also made its way into the tools library. The tools code is not as well organized as the main library, so there's more opportunity for refactoring here.

Also worth noting is that the command-line tools only use public HDF5 API calls, even though they include H5private.h in order to take advantage of the platform-independence scheme we use in the main library and some private utility functions.

There are also a few tools in the high-level library. The gif2h5 and h52gif tools are poorly-written and have known security issues, so they will soon be moved to a separate repository, leaving h5watch as the only high-level command-line tool.

Source code

The source code for the tools likes more like standard C code and uses its own set of macros, which are defined in the tools library header files. There are no FUNC_ENTER macros, you do not need to define a ret_value variable, and the error macros are greatly simplified. Errors are usually handled by a TOOLS_ERROR macro (or TOOLS_GOTO_ERROR if you need to jump to a done: target to handle cleanup). One area where the tools need a lot more work is in handling errors. The tools code frequently ignores errors, often in functions that return void.

A "tools-only" consideration is the use of command-line arguments. We try to be conservative about these, even though they really aren't in the "public API" in the same way as API calls are. Additions and changes to the options will probably result in some discussion.

Tools tests

In most cases, a tool will be run against an input HDF5 file with a particular set of command-line parameters, the exit code checked, and the output compared with a standard output file. In some cases, errors are expected and standard error files will be compared. These standard error files often contain HDF5 error stack dumps, which can cause spurious tool test "failures" when we make changes to the main HDF5 C library.

Test files can be located in a few places in the tools directory tree. Common input files that are used with multiple tools are kept in tools/testfiles. Input files that are used with just one tool are located in tools/test/<tool>/testfiles. Expected output files are located with their respective HDF5 files and end in .txt. Expected error files are also located with their respective HDF5 files and end in .err. h5dump files will usually have .ddl and .xml output files and there will usually be .<tool> files that contain help output. The test files are generated by programs with gentest in their name, though we typically check the generated HDF5 files in instead of recreating them as a part of running the tools tests.

The Autotools aggregate the tools tests in per-tool shell scripts in the tools/test/<tool> directory. Each script starts with a few utility functions that perform setup, compare files, clean output, etc. and the test commands will appear at the end. CMake works similarly, but each test is set up in the CMakeLists.txt and CMakeTests.cmake files in the same directory.

Adding a new test will usually involve:

  • Adding a new function to the appropriate generator program to create a new HDF5 file
  • Adding your new test to the CMake and Autotools test scripts, as described above
  • Adding appropriate output and/or error files for comparison

You MUST add new tests to both the Autotools and CMake.