I much prefer SiliconWizard's example, but with just different whitespace and designated initializers to make it really easy to read:
const struct {
const char *const file;
const char *const name;
const char *const value;
} config[] = {
{ .file = "config.ini", .name = "port1", .value = "9600,8,n,1" },
{ .file = "config.ini", .name = "port2", .value = "19200,8,n,1" },
/* Explicit terminator entry */
{ NULL, NULL, NULL }
};
and the loop to handle these is just
for (size_t i = 0; config[i].file != NULL; i++)
KDE_file_set_config_value(config[i].name, config[i].value, config[i].file);
Compare to your own:
const char *const config[] = {
"config.ini", "port1", "9600,8,n,1",
"config.ini", "port2", "19200,8,n,1",
NULL
};
with the loop being
for (size_t i = 0; config[i] != NULL; i+=3)
KDE_file_set_config_value(config[i+1], config[i+2], config[i+0]);
Considering long-term maintenance, I do believe bugs are far more likely with this latter form than with the former form. Specifically, forgetting which index offset refers to which field. (In these cases, I like to explicitly make the offset visible; therefore the file name is config[i+0] and not config, because the latter would differ from the other two, and possibly overlooked when a subsequent change was made.
The two should generate the same machine code, with the exception of the former config[] array having three and not just one NULL terminator pointers at end.
If these config snippets are made in different compilation unit (different source files) that are just linked together, then I would use ELF sections for it, but it does require ELF object file support from the toolchain. It does not really matter what the final binaries are, as long as the intermediate object files are ELF format.
Then, you could put CONFIGSNIPPET(file,name,value); macros in file or function scope anywhere in the source files, and if that source file is linked to the same executable, those entries are automagically collected to a single array a simple for loop can process in the executable.