I've been thinking about this for a while, and I wanted to gauge how people would feel about introducing a small handful of functions to the public API that allow external programs to dynamically configure the chemistry_data
struct, without directly accessing the struct fields.
New Public Functions
To be a little more concrete, I'm suggesting the introduction of something like the following 3 functions:
// we could also define versions of the following functions that operate on
// the global grackle_data variable
int local_chemistry_data_assign_double(chemistry_data* my_chemistry,
const char* target_name, double val);
int local_chemistry_data_assign_int(chemistry_data* my_chemistry,
const char* target_name, int val);
// this last one may not be worth adding:
int local_chemistry_data_assign_string(chemistry_data* my_chemistry,
const char* target_name, char* val);
The above functions would all generally do the same thing (Each has behavior specialized for a different datatype). In short, the above functions would each:
-
Check if the chemistry_data
struct has a field called target_name
. It would also check if that field matches the type that the function is specialized to handle.
-
If one of the above conditions isn't satisfied, the function returns FAIL
.
-
Otherwise, the function will perform my_chemistry->target_name = val
and
returns SUCCESS
.
Benefits
This API generally provides 2 benefits to external simulation codes that depend on Grackle:
-
This makes it possible for simulation codes to directly forward user specified options from a parameter file directly to Grackle.
-
I'm not sure about other codes, but currently in Enzo-E, users configure Grackle by specifying the name of fields in the chemistry_data
struct parameter and the associated values. When Enzo-E initializes the chemistry_data
struct, there is code that manually goes through (just about) every field in the chemistry_data
struct, checks for a parameter named after the field, and then modifies the value of the chemistry_data
struct, accordingly.
-
This change would let us replace the majority of this manual code, with much simpler code that simply forward the name, value pairs directly to Grackle. An additional benefit of this direct forwarding is that we would no longer need to introduce new code for configuring newly added chemistry_data
fields.
-
This lets simulation codes retain backwards compatibility with older versions of Grackle. Currently, when a code adds support for configuring newly added fields of chemistry_data
, it generally loses the ability to be compiled with older versions of Grackle (since you can't compile have code that tries to access a member of a struct that doesn't exist). There are definitely occasions where I would have personally found this to be useful (when I was using a personal branch of Grackle).
Implementation Challenges
I suspect that the main argument against introducing these functions to the API is that they would introduce another place in the codebase that needs to be updated any time a new field is introduced to chemistry_data
.
With that in mind, I wanted to point out that there is a fairly concise way to do this using a well-established pattern called X-Macros (see here or here for more details). This approach involves maintaining a central list of each struct member that gets reused for implementing multiple functions that requires knowledge of every struct field (A lot of examples show how it can be used for serialization of a struct or printing all the fields of a struct).
I have actually prototyped a solution that leverages this approach. It requires that we maintain a central list of each struct member (and some of its properties) that can be reused in various contexts. You can find an example of this list here for a subset of the fields of chemistry_data
. Each entry in the example holds:
- the name of the field
- a description of that field's type (i.e. whether it's an integer, floating point value or a string)
- the default value of that struct-field (this isn't strictly necessary)
I have provided some examples below that make use of this list. The examples assume that the list is defined in a file called grackle_chemistry_data_fields.def
(with some minor tweaks, we could also include the list directly within the same file where the functions that use it are defined). The example API functions are shown in the following spoiler tag.
Example Implementation
A faster implementation that does less work at runtime (and doesn't involve casts to/from void*
) is definitely possible (it would just involve more code). Since configuration just happens once, this probably doesn't need to be super fast...
#define GET_FIELD_PTR(FIELD,FIELD_TYPE,target,target_type,chem_ptr,out_ptr) \
if ((strcmp(#FIELD, target_name) == 0) & \
(strcmp(#FIELD_TYPE, target_type) == 0)){ \
out_ptr = (void*)&(chem_ptr->FIELD); \
}
// retrieves a pointer to the field of my_chemistry that's named target_name.
//
// This returns a NULL pointer if: my_chemistry is NULL, the field doesn't
// exist, or the field doesn't have the expected type (INT, DOUBLE, STRING)
void* _chem_data_field_ptr(chemistry_data* my_chemistry,
const char* target_name, const char* target_type)
{
if (my_chemistry == NULL) { return NULL; }
void* field_ptr = NULL;
// now try to retrieve field_ptr from chemistry_data
#define ENTRY(FIELD, TYPE, DEFAULT_VAL) \
GET_FIELD_PTR (FIELD,TYPE,target_name,target_type,my_chemistry,field_ptr)
#include "grackle_chemistry_data_fields.def"
#undef ENTRY
return field_ptr;
}
int local_chemistry_data_assign_double(chemistry_data* my_chemistry,
const char* target_name, double val)
{
void* field_ptr = _chem_data_field_ptr(my_chemistry, target_name, "DOUBLE");
if (field_ptr == NULL) { return FAIL; }
*((double*)field_ptr) = val;
return SUCCESS;
}
int local_chemistry_data_assign_int(chemistry_data* my_chemistry,
const char* target_name, int val)
{
void* field_ptr = _chem_data_field_ptr(my_chemistry, target_name, "INT");
if (field_ptr == NULL) { return FAIL; }
*((int*)field_ptr) = val;
return SUCCESS;
}
While you still need to keep the X-Macro List up to date with all the fields in chemistry_data
, you can use this list to drastically simplify the implementation of some other existing functions The 2 main examples are provided below:
Simplified _set_default_chemistry_parameters function
The current implementation can be found here
chemistry_data _set_default_chemistry_parameters(void)
{
chemistry_data my_chemistry;
#define ENTRY(FIELD, TYPE, DEFAULT_VAL) my_chemistry.FIELD = DEFAULT_VAL;
#include "grackle_chemistry_data_fields.def"
#undef ENTRY
return my_chemistry;
}
Simplified show_parameters function
The current implementation can be found here
void _show_field_INT(FILE *fp, const char* field, int val)
{ fprintf(fp, "%-33s = %d\n", field, val); }
void _show_field_DOUBLE(FILE *fp, const char* field, double val)
{ fprintf(fp, "%-33s = %g\n", field, val); }
void _show_field_STRING(FILE *fp, const char* field, const char* val)
{ fprintf(fp, "%-33s = %s\n", field, val); }
void show_parameters(FILE *fp, chemistry_data *my_chemistry){
#define ENTRY(FIELD, TYPE, DEFAULT_VAL) \
_show_field_ ## TYPE (fp, #FIELD, my_chemistry->FIELD);
#include "grackle_chemistry_data_fields.def"
#undef ENTRY
}
In case you find it helpful, here is a link that put's the above code snippets together into a toy executable (Just untar the folder and call gcc examples.c
to compile the executable).
I'm happy to personally create the PR introducing this change, but I wanted to gauge interest in this before I do so (it wouldn't involve much more work). In particular, I realize this X-Macro approach may be a little controversial and that there could be other objections to expanding the API.