As well as the high-level CCTK_* routines, Cactus also provides a set of lower-level Util_* utility routines, which are mostly independent of the rest of Cactus. This chapter gives a general overview of programming with these utility routines.
Cactus functions may need to pass information through a generic interface. In the past, we have used various ad hoc means to do this, and we often had trouble passing ”extra” information that wasn’t anticipated in the original design. For example, for periodic output of grid variables, CCTK_OutputVarAsByMethod() requires that any parameters (such as hyperslabbing parameters) be appended as an option string to the variable’s character string name. Similarly, elliptic solvers often need to pass various parameters, but we haven’t had a good way to do this.
Key/value tables (tables for short) provide a clean solution to these problems. They’re implemented by the Util_Table* functions (described in detail in the Reference Manual).
Basically, a table is an object which maps strings to almost arbitrary user-defined data. (If you know Perl, a table is very much like a Perl hash table. Alternatively, if you know Unix shells, a table is like the set of all environment variables. As yet another analogy, if you know Awk, a table is like an Awk associative array.)1
More formally, a table is an object which stores a set of keys and a corresponding set of values. We refer to a (key,value) pair as a table entry.
Keys are C-style null-terminated character strings, with the slash character ‘/’ reserved for future expansion.2
Values are 1-dimensional arrays of any of the usual Cactus data types, described in Section C1.9.8. A string can be stored by treating it as a 1-dimensional array of CCTK_CHAR (there’s an example of this below).
The basic “life cycle” of a table looks like this:
There are also convenience functions Util_TableSetFromString() to set entries in a table based on a parameter-file-style string, and Util_TableCreateFromString() to create a table and then set entries in it based on a parameter-file-style string.
As well, there are “table iterator” functions Util_TableIt*() to allow manipulation of a table even if you don’t know its keys.
A table has an integer “flags word” which may be used to specify various options, via bit flags defined in util_Table.h. For example, the flags word can be used to control whether keys should be compared as case sensitive or case insensitive strings. See the detailed function description of Util_TableCreate() in the Reference Manual for a list of the possible bit flags and their semantics.
Here’s a simple example (in C)3 of how to use a table:
Actually, you shouldn’t write code like this—in the real world errors sometimes happen, and it’s much better to catch them close to their point of occurrence, rather than silently produce garbage results or crash your program. So, the right thing to do is to always check for errors. To allow this, all the table routines return a status, which is zero or positive for a successful return, but negative if and only if some sort of error has occurred.4 So, the above example should be rewritten like this:
As well as a single numbers (or characters or pointers), tables can also store 1-dimensional arrays of numbers (or characters or pointers).5
For example (continuing the previous example):
As you can see, a table entry remembers the length of any array value that has been stored in it.6
If you only want the first few values of a larger array, just pass in the appropriate length of your array, that’s OK:
You can even ask for just the first value:
One very common thing you might want to store in a table is a character string. While you could do this by explicitly storing an array of CCTK_CHAR, there are also routines specially for conveniently setting and getting strings:
Util_TableGetString() guarantees that the string is terminated by a null character (‘\0’), and also returns an error if the string is too long for the buffer.
There are also convenience routines for the common case of setting values in a table based on a string.
For example, the following code sets up exactly the same table as the example in Section D3.2.3:
There is also an even higher-level convenience function Util_TableCreateFromString(): this creates a table with the case insensitive flag set (to match Cactus parameter file semantics), then (assuming no errors occurred) calls Util_TableSetFromString() to set values in the table.
For example, the following code sets up a table (with the case insensitive flag set) with four entries: an integer number (two), a real number (pi), a string (buffer), and an integer array with three elements (array):
Note that this code passes a single string to Util_TableCreateFromString()7, which then gets parsed into key/value pairs, with the key separated from its corresponding value by an equals sign.
Values for numbers are converted into integers (CCTK_INT) if possible (no decimal point appears in the value), otherwise into reals (CCTK_REAL). Strings must be enclosed in either single or double quotes. String values in single quotes are interpreted literally, strings in double quotes may contain character escape codes which then will be interpreted as in C. Arrays must be enclosed in curly braces, array elements must be single numbers of the same type (either all integer or all real).
In the examples up to now, the code, which wanted to get values from the table, knew what the keys were. It’s also useful to be able to write generic code which can operate on a table without knowing the keys. “Table iterators” (“iterators”, for short) are used for this.
An iterator is an abstraction of a pointer to a particular table entry. Iterators are analogous to the DIR * pointers used by the POSIX opendir(), readdir(), closedir(), and similar functions, to Perl hash tables’ each(), keys(), and values(), and to the C++ Standard Template Library’s forward iterators.
At any time, the entries in a table may be considered to be in some arbitrary (implementation-defined) order; an iterator may be used to walk through some or all of the table entries in this order. This order is guaranteed to remain unchanged for any given table, so long as no changes are made to that table, i.e. so long as no Util_TableSet*(), Util_TableSet*Array(), Util_TableSetGeneric(), Util_TableSetGenericArray(), Util_TableSetString(), or Util_TableDeleteKey() calls are made on that table (making such calls on other tables doesn’t matter). The order may change if there is any change in the table, and it may differ even between different tables with identical key/value contents (including those produced by Util_TableClone()).8
Any change in the table also invalidates all iterators pointing anywhere in the table; using any such iterator is an error. Multiple iterators may point into the same table; they all use the same order, and (unlike in Perl) they’re all independent.
The detailed function description in the Reference Manual for Util_TableItQueryKeyValueInfo() has an example of using an iterator to print out all the entries in a table.
At the moment, the table functions are not thread-safe in a multithreaded environment.
Note that tables and iterators are process-wide, i.e. all threads see the same tables and iterators (think of them as like the Unix current working directory, with the various routines which modify the table or change iterators acting like a Unix chdir() system call).
In a multiprocessor environment, tables are always processor-local.
Tables do not themselves have names or other attributes. However, we may add some special “system tables” to be used by Cactus itself to store this sort of information for those cases where it’s needed. For example, we may add support for a “checkpoint me” bit in a table’s flags word, so that if you want a table to be checkpointed, you just need to set this bit. In this case, the table will probably get a system generated name in the checkpoint dump file. But if you want the table to have some other name in the dump file, then you need to tell the checkpointing code that, by setting an appropriate entry in a checkpoint table. (You would find the checkpoint table by looking in a special “master system table” that records handles of other interesting tables.)
1However, the present Cactus tables implementation is optimized for a relatively small number of distinct keys in any one table. It will still work OK for huge numbers of keys, but it will be slow.
2Think of hierarchical tables for storing tree-like data structures.
3All (or almost all) of the table routines are also usable from Fortran. See the full descriptions in the Reference Manual for details.
4Often (as in the examples here) you don’t care about the details of which error occurred. But if you do, there are various error codes defined in util_Table.h and util_ErrorCodes.h; the detailed function descriptions in the Reference Manual say which error codes each function can return.
5Note that the table makes (stores) a copy of the array you pass in, so it’s somewhat inefficient to store a large array (e.g. a grid function) this way. If this is a problem, consider storing a CCTK_POINTER (pointing to the array) in the table instead. (Of course, this requires that you ensure that the array still exists whenever that CCTK_POINTER is used.)
6In fact, actually all table values are arrays—setting or getting a single value is just the special case where the array length is 1.
7C automatically concatenates adjacent character string constants separated only by whitespace.
8For example, if tables were implemented by hashing, the internal order could be that of the hash buckets, and the hash function could depend on the internal table address.