If you have separate print functions for each type you use, then in C11 and later one can use preprocessor macros and _Generic to implement a variadic print() macro that expands to individual function calls; i.e.
print("x = ", x, ", y = ", y, "\n");
expanding to say
print_cs("x = ");
print_int(x);
print_cs(", y = ");
print_float(y);
print_cs("\n");
I personally do not bother, because the multi-function sequence is just as readable to me as the single-function one.
For embedded uses, I like to use an ELF section containing a description of the variables, i.e.
struct var_desc {
void *const ref;
const char *const name;
int_fast8_t (*const get)(uint_fast8_t maxlen, char buf[maxlen], void *ref);
int_fast8_t (*const set)(uint_fast8_t maxlen, char buf[maxlen], void *ref);
};
// Getters convert the referred to variable to a string into buf
extern int_fast8_t vardesc_get_int(uint_fast8_t maxlen, char buf[maxlen], void *ref);
extern int_fast8_t vardesc_get_float(uint_fast8_t maxlen, char buf[maxlen], void *ref);
// Setters convert the string to the referred to variable
extern int_fast8_t vardesc_set_int(uint_fast8_t maxlen, char buf[maxlen], void *ref);
extern int_fast8_t vardesc_set_float(uint_fast8_t maxlen, char buf[maxlen], void *ref);
#define MERGE4_(a,b,c,d) a ## b ## c ## d
#define MERGE4(a,b,c,d) MERGE4_(a,b,c,d)
#define VAR_DESC_NAME(prefix) MERGE4(__, prefix, _, __LINE__)
#define DECLARE_VAR(_var, _name, _getter, _setter) \
static const struct var_desc VAR_DESC_NAME(vardesc) \
__attribute__((section ("vardesc"), used)) = { \
.ref = &(_var), \
.name = _name, \
.get = _getter, \
.set = _setter, \
}
// Note: We could use _Generic for autoselection of .get and .set functions (based on _var),
// and stringify variable name. Sometimes, the name will differ, for example with a subsystem prefix.
extern const struct var_desc __start_vardesc[];
extern const struct var_desc __stop_vardesc[];
#define __num_vardesc (uintptr_t)(__stop_vardesc - __start_vardesc)
so that in the final linked binary, the vardesc section will collect all such declarations from all object files into a single contiguous array. A simple text interface can then query and set any of these variables. The above assumes that the linker script exports __start_vardesc at the beginning of the section, and __stop_vardesc at the address just past the section, as is usual; and this section can/should reside in Flash.
For example, you might have
static int step_count;
DECLARE_VAR(step_count, "step_count", vardesc_get_int, vardesc_set_int);
either in a global scope, or in a function scope. Note that the variable itself does not have global linkage, nor does the __vardesc_N static structure the declaration generates.
If one omits the setter, and adds say a debug level value for each, then this can easily be used for state dumping. You might also use a char array instead of a pointer for the .name member so that the strings will also be included in the same section.
(The order of these entries will vary depending on compiler and linker version, and although it is possible to sort these so that lookup is O(log2N) instead of linear, sorting them is a bit annoying to do as it requires direct ELF object file modification. My approach is to read the final linked ELF object file, then generate a new C source file with the actual array, that when compiled, reproduces the section, with a different section name. The final linking is then redone, dropping the original section, and including the recompiled section. This approach is very robust, and pretty portable across architectures.)
This is particularly useful when you have a build system that conditionally compiles and/or links in objects depending on build configuration, because this way the debug interface does not pull in all code, and does not need to know the build configuration.
I obviously use the same for command-response interfaces, where each command is described using a similar structure. That way, commands do not need to be listed and edited in a central array or list, and instead can be described in their source files. The actual descriptor array is then constructed at link time by the linker, with the array stored in Flash, nor RAM.