├── .github └── workflows │ └── main.yml ├── LICENSE.txt ├── README.md ├── cnode.h ├── example ├── .gitignore ├── Makefile ├── check.py ├── config.toml ├── config_spec.yml └── main.cpp ├── gen_config.py ├── infographic.png └── meson.build /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: main 2 | on: ['push', 'pull_request'] 3 | jobs: 4 | main: 5 | name: Build and Test 6 | runs-on: ubuntu-latest 7 | steps: 8 | - name: Checkout 9 | uses: actions/checkout@v3 10 | - name: Build 11 | run: | 12 | pip install --user pyyaml tomli 13 | cd example 14 | make 15 | ./example 16 | ./check.py 17 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2022 Matt Borgerson 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | GenConfig 2 | ========= 3 | Aims to provide less-painful C/C++ app configuration support. 4 | 5 | ![](infographic.png) 6 | 7 | Motivation 8 | ---------- 9 | Created because all of the config systems I have used (or created) in the past had too many pain points, like: 10 | - Requiring things to be defined in multiple places 11 | - Not using standard, human-friendly configuration formats 12 | - Lacking validation of basic types and structure 13 | - Not supporting hierarchical data structures, arrays, strings very well 14 | - Requiring tedious structure maintenance 15 | - Requiring accessor calls to get at simple data fields 16 | - Not supporting configuration deltas 17 | 18 | This system attempts to alleviate those pain points and make configuration suck less: 19 | - Automatically generates structure definition code from an input specification, which is easy to write 20 | - Loads application configuration from [TOML](https://toml.io/en/) format, which is standardized and human-friendly 21 | - Validates user configuration matches expected structure and fields are of expected type 22 | - Support hierarchical structures of basic types: `bool`, `int`, `float`, `enum`, strings (UTF-8), and arrays 23 | - Minimizes development effort: just define the setting once in a spec file and the rest is handled 24 | - Usable in C/C++, without having to do any lookups or extra validation: just read from a `struct` 25 | - Supports saving configuration deltas 26 | 27 | How it Works 28 | ------------ 29 | - You define the options you want for your app in a spec file 30 | - You run `gen_config.py` to generate a header file with `struct config` definition 31 | - User runs your app, providing their settings in a `.toml` file matching your specification 32 | - Your code calls a function to load, parse, and store the config file to the `config` structure 33 | - Your code accesses the config by just reading from and writing to the `config` struct 34 | - Your code changes `config` structure in response to some user action 35 | - Your code calls a function to save the config delta to the user's `.toml` config file 36 | - You retain a bit more hair that you might have ripped out using/creating another configuration system 37 | - Your app makes lots of money and you send some my way because you support open-source development. Thanks, that's nice of you. 38 | 39 | Requirements 40 | ------------ 41 | - [toml++](https://marzer.github.io/tomlplusplus/), which is used to parse config file 42 | - Compiler that supports C++17 43 | 44 | Example 45 | ------- 46 | This is the specification file that defines your config options: 47 | 48 | ```yaml 49 | company: 50 | name: string 51 | headquarters: 52 | state: string 53 | city: string 54 | 55 | products: 56 | type: array 57 | items: 58 | name: 59 | type: string 60 | default: New Product 61 | price: float 62 | inventory: int 63 | international_shipping: 64 | type: bool 65 | default: true 66 | suppliers: 67 | type: array 68 | items: string 69 | category: 70 | type: enum 71 | values: ['fruit', 'vegetable', 'beverage', 'explosive'] 72 | ``` 73 | 74 | After running `gen_config.py`, this C code is generated automatically. 75 | 76 | ```C 77 | enum CONFIG_COMPANY_PRODUCTS_CATEGORY { 78 | CONFIG_COMPANY_PRODUCTS_CATEGORY_FRUIT, 79 | CONFIG_COMPANY_PRODUCTS_CATEGORY_VEGETABLE, 80 | CONFIG_COMPANY_PRODUCTS_CATEGORY_BEVERAGE, 81 | CONFIG_COMPANY_PRODUCTS_CATEGORY_EXPLOSIVE, 82 | CONFIG_COMPANY_PRODUCTS_CATEGORY__COUNT 83 | }; 84 | 85 | struct config { 86 | struct company { 87 | const char *name; 88 | struct headquarters { 89 | const char *state; 90 | const char *city; 91 | } headquarters; 92 | struct products { 93 | const char *name; 94 | float price; 95 | int inventory; 96 | bool international_shipping; 97 | const char **suppliers; 98 | unsigned int suppliers_count; 99 | enum CONFIG_COMPANY_PRODUCTS_CATEGORY category; 100 | } *products; 101 | unsigned int products_count; 102 | } company; 103 | }; 104 | ``` 105 | 106 |
107 | 108 | Additionally, a corresponding `CNode` tree is created, that's used to support everything: 109 | 110 | 111 | ```cpp 112 | CNode config_tree = 113 | ctab("config", { 114 | ctab("company", { 115 | cstring( 116 | offsetof(struct config, company.name), 117 | "name", ""), 118 | ctab("headquarters", { 119 | cstring( 120 | offsetof(struct config, company.headquarters.state), 121 | "state", ""), 122 | cstring( 123 | offsetof(struct config, company.headquarters.city), 124 | "city", "") 125 | }), 126 | carray( 127 | offsetof(struct config, company.products), 128 | offsetof(struct config, company.products_count), 129 | sizeof(struct config::company::products), 130 | "products", 131 | ctab("", { 132 | cstring( 133 | offsetof(struct config::company::products, name), 134 | "name", "New Product"), 135 | cnumber( 136 | offsetof(struct config::company::products, price), 137 | "price", 0.0), 138 | cinteger( 139 | offsetof(struct config::company::products, inventory), 140 | "inventory", 0), 141 | cbool( 142 | offsetof(struct config::company::products, international_shipping), 143 | "international_shipping", true), 144 | carray( 145 | offsetof(struct config::company::products, suppliers), 146 | offsetof(struct config::company::products, suppliers_count), 147 | sizeof(((struct config::company::products *){0})->suppliers[0]), 148 | "suppliers", 149 | cstring( 150 | 0, "", "") 151 | ), 152 | cenum( 153 | offsetof(struct config::company::products, category), 154 | "category", {"fruit", "vegetable", "beverage", "explosive"}, "fruit") 155 | }) 156 | ) 157 | }) 158 | }); 159 | ``` 160 | 161 |
162 | 163 | Config file to be loaded at runtime, in [TOML](https://toml.io/en/) format: 164 | 165 | ```toml 166 | [company] 167 | name = 'Acme Corp' 168 | products = [ 169 | { name = 'Apple', price = 1.2, inventory = 100, suppliers = ['Midwest Orchard', 'Tasty Apples Inc.'], category = 'fruit' }, 170 | { name = 'TNT', price = 50, inventory = 1000, category = 'explosive', international_shipping = false }, 171 | ] 172 | 173 | [company.headquarters] 174 | city = 'Phoenix' 175 | state = 'Arizona' 176 | ``` 177 | 178 | Loading the config basically looks like: 179 | 180 | ```cpp 181 | #include // Required for basic parsing 182 | #include // Required data structure for config mgmt 183 | 184 | #define DEFINE_CONFIG_TREE 185 | #include "config.h" 186 | 187 | // Load config from user file 188 | struct config s; 189 | auto toml_table = toml::parse_file("config.toml"); 190 | config_tree.update_from_table(toml_table); 191 | config_tree.store_to_struct(&s); 192 | ``` 193 | 194 | Then when you are ready to save the config again: 195 | 196 | ```cpp 197 | // Update config tree from structure if you have modified it 198 | config_tree.update_from_struct(&s); 199 | 200 | // Save config 201 | FILE *f = fopen("config.toml", "wb"); 202 | fprintf(f, "%s", config_tree.generate_delta_toml().c_str()); 203 | ``` 204 | -------------------------------------------------------------------------------- /cnode.h: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022 Matt Borgerson 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in all 11 | // copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | // SOFTWARE. 20 | #pragma once 21 | 22 | #include 23 | #include 24 | #include 25 | #include 26 | #include 27 | 28 | #include 29 | #include 30 | #include 31 | #include 32 | #include 33 | #include 34 | #include 35 | 36 | // 3rdparty 37 | #include 38 | 39 | #ifndef DEBUG 40 | #define DEBUG 0 41 | #endif 42 | 43 | template< class T > 44 | std::unique_ptr copy_unique(const std::unique_ptr& source) 45 | { 46 | return source ? std::make_unique(*source) : nullptr; 47 | } 48 | 49 | template 50 | std::string string_format( const std::string& format, Args ... args ) 51 | { 52 | int size_s = std::snprintf( nullptr, 0, format.c_str(), args ... ) + 1; // Extra space for '\0' 53 | if( size_s <= 0 ){ throw std::runtime_error( "Error during formatting." ); } 54 | auto size = static_cast( size_s ); 55 | std::unique_ptr buf( new char[ size ] ); 56 | std::snprintf( buf.get(), size, format.c_str(), args ... ); 57 | return std::string( buf.get(), buf.get() + size - 1 ); // We don't want the '\0' inside 58 | } 59 | 60 | typedef enum CNodeType { 61 | Array, Boolean, Enum, Integer, Number, String, Table, 62 | } CNodeType; 63 | 64 | char const * const type_names[] = { 65 | "Array", "Boolean", "Enum", "Integer", "Number", "String", "Table", 66 | }; 67 | 68 | class CNode { 69 | public: 70 | CNodeType type; 71 | std::string name; 72 | std::vector children; 73 | union { 74 | struct { bool val, default_val; } boolean; 75 | struct { float min, max, val, default_val; } number; 76 | struct { int min, max, val, default_val; } integer; 77 | } data; 78 | struct { std::string val, default_val; } string; 79 | std::unique_ptr array_item_type; 80 | struct { std::vector values; int val, default_val; } data_enum; 81 | 82 | struct { 83 | size_t offset, count_offset, size; 84 | } serialized; 85 | 86 | CNode(const CNode& other) 87 | { 88 | type = other.type; 89 | name = std::string(other.name); 90 | children = std::vector(other.children); 91 | array_item_type = copy_unique(other.array_item_type); 92 | data = other.data; 93 | data_enum = other.data_enum; 94 | string = other.string; 95 | serialized = other.serialized; 96 | } 97 | 98 | CNode& operator=(const CNode& other) 99 | { 100 | type = other.type; 101 | name = std::string(other.name); 102 | children = std::vector(other.children); 103 | array_item_type = copy_unique(other.array_item_type); 104 | data = other.data; 105 | data_enum = other.data_enum; 106 | string = other.string; 107 | serialized = other.serialized; 108 | 109 | return *this; 110 | } 111 | 112 | CNode(std::string name, std::vector children = {}) 113 | : type(CNodeType::Table), name(name), children(children) {} 114 | 115 | CNode(std::string name, CNodeType type) 116 | : type(type), name(name) {} 117 | 118 | // 119 | // Get child node by name 120 | // 121 | CNode *child(std::string_view needle) { 122 | for (auto &c : children) { 123 | if (!needle.compare(c.name)) { 124 | return &c; 125 | } 126 | } 127 | return NULL; 128 | } 129 | 130 | // 131 | // Map the enumerated type `value` to the corresponding integer 132 | // 133 | int enum_str_to_int(std::string value) 134 | { 135 | auto &v = data_enum.values; 136 | auto it = std::find(v.begin(), v.end(), value); 137 | return (it != v.end()) ? it - v.begin() : -1; 138 | } 139 | 140 | // 141 | // Print indentation to stdout 142 | // 143 | void indent(int c) { 144 | while (c--) printf(" "); 145 | } 146 | 147 | // 148 | // Print a representation of the tree to stdout 149 | // 150 | void repr(int depth = 0) { 151 | indent(depth); printf("%s<%s> ", name.c_str(), type_names[type]); 152 | 153 | if (type == Table) { 154 | printf("\n"); 155 | for (auto &c : children) { 156 | c.repr(depth + 1); 157 | } 158 | return; 159 | } 160 | 161 | printf("@%zd ", serialized.offset); 162 | 163 | const char *tf_str[2] = { "false", "true" }; 164 | switch (type) { 165 | case Array: 166 | printf("%zdB {\n", serialized.size); 167 | array_item_type->repr(depth+1); 168 | indent(depth); printf("}\n"); 169 | return; 170 | case Boolean: printf("%s (d=%s)", tf_str[data.boolean.val], tf_str[data.boolean.default_val]); break; 171 | case Enum: 172 | printf("%s (d=%s of { ", 173 | data_enum.values[data_enum.val].c_str(), 174 | data_enum.values[data_enum.default_val].c_str()); 175 | for (unsigned int i = 0; i < data_enum.values.size(); i++) { 176 | if (i) printf(", "); 177 | printf("%s ", data_enum.values[i].c_str()); 178 | } 179 | printf("})"); 180 | break; 181 | case Integer: printf("%d (d=%d)", data.integer.val, data.integer.default_val); break; 182 | case Number: printf("%g (d=%g)", data.number.val, data.number.default_val); break; 183 | case String: printf("\"%s\" (d=\"%s\")", string.val.c_str(), string.default_val.c_str()); break; 184 | default: 185 | assert(false); 186 | break; 187 | } 188 | printf("\n"); 189 | } 190 | 191 | // 192 | // Update tree values from the given TOML table 193 | // 194 | void update_from_table(const toml::table &tbl, int depth = 0, std::string path = "") 195 | { 196 | for (auto&& [k, v] : tbl) { 197 | std::string cpath = path.length() ? path + "." + std::string(k) 198 | : std::string(k); 199 | 200 | #if DEBUG 201 | fprintf(stderr, "Currently at %s\n", cpath.c_str()); 202 | #endif 203 | 204 | CNode *cnode = child(k.str()); 205 | if (!cnode) { 206 | fprintf(stderr, "Warning: unrecognized "); 207 | report_key_line_col(cpath, v.source()); 208 | fprintf(stderr, "\n"); 209 | continue; 210 | } 211 | 212 | if (!check_type(v, cnode->type)) { 213 | fprintf(stderr, "Error: incorrect type for "); 214 | report_key_line_col(cpath, v.source()); 215 | fprintf(stderr, "\n"); 216 | continue; 217 | } 218 | 219 | if (v.is_table()) { 220 | cnode->update_from_table(*v.as_table(), depth+1, cpath); 221 | continue; 222 | } 223 | 224 | if (cnode->type == Boolean) { 225 | cnode->set_boolean_tv(*v.value(), v.source(), cpath); 226 | } else if (cnode->type == Enum) { 227 | cnode->set_enum_tv(*v.value(), v.source(), cpath); 228 | } else if (cnode->type == Integer) { 229 | cnode->set_integer_tv(*v.value(), v.source(), cpath); 230 | } else if (cnode->type == Number) { 231 | cnode->set_number_tv(*v.value(), v.source(), cpath); 232 | } else if (cnode->type == String) { 233 | cnode->set_string_tv(*v.value(), v.source(), cpath); 234 | } else if (v.is_array()) { 235 | auto arr = v.as_array(); 236 | int len = arr->size(); 237 | cnode->children.clear(); 238 | if (len > 0) { 239 | int i = 0; 240 | for (const toml::node &elem : *arr) { 241 | if (!check_type(elem, cnode->array_item_type->type)) { 242 | fprintf(stderr, "Error: Unexpected array entry type at "); 243 | report_line_col(elem.source()); 244 | fprintf(stderr, "\n"); 245 | continue; 246 | } 247 | cnode->children.push_back(*cnode->array_item_type); 248 | 249 | if (cnode->array_item_type->type == Table) { 250 | char buf[8]; 251 | snprintf(buf, sizeof(buf), "[%d]", i); 252 | cnode->children.back().update_from_table(*elem.as_table(), depth+1, cpath + buf); 253 | // FIXME: If the item is invalid this leaves default values initialized. 254 | // add an error flag for it 255 | } else if (cnode->array_item_type->type == Boolean) { 256 | cnode->children.back().set_boolean_tv(*elem.value(), elem.source(), cpath); 257 | } else if (cnode->array_item_type->type == Enum) { 258 | cnode->children.back().set_enum_tv(*elem.value(), elem.source(), cpath); 259 | } else if (cnode->array_item_type->type == Integer) { 260 | cnode->children.back().set_integer_tv(*elem.value(), elem.source(), cpath); 261 | } else if (cnode->array_item_type->type == Number) { 262 | cnode->children.back().set_number_tv(*elem.value(), elem.source(), cpath); 263 | } else if (cnode->array_item_type->type == String) { 264 | cnode->children.back().set_string_tv(*elem.value(), elem.source(), cpath); 265 | } else { 266 | assert(false); 267 | } 268 | i++; 269 | } 270 | } 271 | } else { 272 | assert(false); 273 | } 274 | } 275 | } 276 | 277 | // 278 | // Check that the toml node type matches the expected CNode type 279 | // 280 | bool check_type(const toml::node &v, CNodeType expected) 281 | { 282 | switch (expected) { 283 | case Array: return v.is_array(); 284 | case Boolean: return v.is_boolean(); 285 | case Enum: return v.is_string(); 286 | case Integer: return v.is_integer(); 287 | case Number: return v.is_number(); 288 | case String: return v.is_string(); 289 | case Table: return v.is_table(); 290 | default: assert(false); 291 | } 292 | } 293 | 294 | // 295 | // Output "line y column z" to stderr for given input source region 296 | // 297 | void report_line_col(const toml::source_region &src) { 298 | fprintf(stderr, "line %d column %d", src.begin.line, src.begin.column); 299 | } 300 | 301 | // 302 | // Output "key 'x' at line y column z" to stderr for given input source region 303 | // 304 | void report_key_line_col(const std::string &key, const toml::source_region &src) 305 | { 306 | fprintf(stderr, "key '%s' at ", key.c_str()); 307 | report_line_col(src); 308 | } 309 | 310 | // 311 | // Set boolean value 312 | // 313 | void set_boolean_tv(bool v, const toml::source_region &from, std::string path) 314 | { 315 | #if DEBUG 316 | fprintf(stderr, "%s<%s> = %d at ", path.c_str(), type_names[type], v); 317 | report_line_col(from); 318 | fprintf(stderr, "\n"); 319 | #endif 320 | data.boolean.val = v; 321 | } 322 | 323 | // 324 | // Set enumerated type value 325 | // 326 | void set_enum_by_index(int idx) 327 | { 328 | data_enum.val = idx; 329 | } 330 | 331 | void set_enum_tv(std::string v, const toml::source_region &from, std::string path) 332 | { 333 | int idx = enum_str_to_int(v); 334 | if (idx < 0) { 335 | fprintf(stderr, "Error: invalid value for "); 336 | report_key_line_col(path, from); 337 | fprintf(stderr, "\n"); 338 | return; 339 | } 340 | 341 | #if DEBUG 342 | fprintf(stderr, "%s<%s> = %s at ", path.c_str(), type_names[type], v.c_str()); 343 | report_line_col(from); 344 | fprintf(stderr, "\n"); 345 | #endif 346 | set_enum_by_index(idx); 347 | } 348 | 349 | // 350 | // Set integer value 351 | // 352 | void set_integer(int v) 353 | { 354 | data.integer.val = v; 355 | } 356 | 357 | void set_integer_tv(int v, const toml::source_region &from, std::string path) 358 | { 359 | #if DEBUG 360 | fprintf(stderr, "%s<%s> = %d at ", path.c_str(), type_names[type], v); 361 | report_line_col(from); 362 | fprintf(stderr, "\n"); 363 | #endif 364 | set_integer(v); 365 | } 366 | 367 | // 368 | // Set number value 369 | // 370 | void set_number(float v) 371 | { 372 | data.number.val = v; 373 | } 374 | 375 | void set_number_tv(float v, const toml::source_region &from, std::string path) 376 | { 377 | #if DEBUG 378 | fprintf(stderr, "%s<%s> = %g at ", path.c_str(), type_names[type], v); 379 | report_line_col(from); 380 | fprintf(stderr, "\n"); 381 | #endif 382 | set_number(v); 383 | } 384 | 385 | // 386 | // Set string value 387 | // 388 | void set_string(std::string v) 389 | { 390 | string.val = v; 391 | } 392 | 393 | void set_string_tv(std::string v, const toml::source_region &from, std::string path) 394 | { 395 | #if DEBUG 396 | fprintf(stderr, "%s<%s> = '%s' at ", path.c_str(), type_names[type], v.c_str()); 397 | report_line_col(from); 398 | fprintf(stderr, "\n"); 399 | #endif 400 | set_string(v); 401 | } 402 | 403 | // 404 | // Store values of this node and its children to the structure `s` associated with the tree 405 | // 406 | void store_to_struct(void *s) 407 | { 408 | uint8_t *p = (uint8_t *)s + serialized.offset; 409 | #if DEBUG 410 | fprintf(stderr, "Storing %s to offset %d @ %p\n", name.c_str(), serialized.offset, p); 411 | #endif 412 | 413 | switch (type) { 414 | case Array: { 415 | int *pc = (int *)((uint8_t *)s + serialized.count_offset); 416 | *pc = children.size(); 417 | if (children.size()) { 418 | *(void**)p = calloc(children.size(), serialized.size); 419 | p = (uint8_t *) *(void**)p; 420 | for (unsigned int i = 0; i < children.size(); i++) { 421 | children[i].store_to_struct(p); 422 | p += serialized.size; 423 | } 424 | } else { 425 | *(void**)p = NULL; 426 | } 427 | break; 428 | } 429 | case Boolean: *(bool*)p = data.boolean.val; break; 430 | case Enum: *(int*)p = data_enum.val; break; 431 | case Integer: *(int*)p = data.integer.val; break; 432 | case Number: *(float*)p = data.number.val; break; 433 | case String: *(char**)p = strdup(string.val.c_str()); break; 434 | case Table: 435 | for (auto &c : children) { 436 | c.store_to_struct(s); 437 | } 438 | break; 439 | default: assert(false); 440 | } 441 | } 442 | 443 | // 444 | // Free any allocations made 445 | // 446 | void free_allocations(void *s) 447 | { 448 | uint8_t *p = (uint8_t *)s + serialized.offset; 449 | #if DEBUG 450 | fprintf(stderr, "Free %s offset %d @ %p\n", name.c_str(), serialized.offset, p); 451 | #endif 452 | 453 | switch (type) { 454 | case Array: { 455 | int *pc = (int *)((uint8_t *)s + serialized.count_offset); 456 | *pc = 0; 457 | if (children.size()) { 458 | uint8_t *p_c = (uint8_t *) *(void**)p; 459 | for (unsigned int i = 0; i < children.size(); i++) { 460 | children[i].free_allocations(p_c); 461 | p_c += serialized.size; 462 | } 463 | 464 | free(*(void**)p); 465 | } 466 | *(void**)p = NULL; 467 | break; 468 | } 469 | case String: 470 | free(*(void**)p); 471 | *(void**)p = NULL; 472 | break; 473 | case Table: 474 | for (auto &c : children) { 475 | c.free_allocations(s); 476 | } 477 | break; 478 | default: break; 479 | } 480 | } 481 | 482 | // 483 | // Update values of this node and its children from the structure `s` associated with the tree 484 | // 485 | void update_from_struct(void *s) 486 | { 487 | uint8_t *p = (uint8_t *)s + serialized.offset; 488 | #if DEBUG 489 | fprintf(stderr, "Loading %s from offset %d @ %p\n", name.c_str(), serialized.offset, p); 490 | #endif 491 | 492 | switch (type) { 493 | case Array: { 494 | int *pc = (int *)((uint8_t *)s + serialized.count_offset); 495 | children.clear(); 496 | p = (uint8_t *) *(void**)p; 497 | for (int i = 0; i < *pc; i++) { 498 | children.push_back(*array_item_type); 499 | children.back().update_from_struct(p); 500 | p += serialized.size; 501 | } 502 | break; 503 | } 504 | case Boolean: data.boolean.val = *(bool*)p; break; 505 | case Enum: data_enum.val = *(int*)p; break; 506 | case Integer: data.integer.val = *(int*)p; break; 507 | case Number: data.number.val = *(float*)p; break; 508 | case String: string.val = std::string(*(char**)p); break; 509 | case Table: 510 | for (auto &c : children) { 511 | c.update_from_struct(s); 512 | } 513 | break; 514 | default: assert(false); 515 | } 516 | } 517 | 518 | // 519 | // Check if this node's value differs from its default value 520 | // 521 | // Note: Arrays of size > 0 are always considered differing 522 | // 523 | bool differs_from_default() 524 | { 525 | switch (type) { 526 | case Array: return children.size() > 0; 527 | case Boolean: return data.boolean.val != data.boolean.default_val; 528 | case Enum: return data_enum.val != data_enum.default_val; 529 | case Integer: return data.integer.val != data.integer.default_val; 530 | case Number: return data.number.val != data.number.default_val; 531 | case String: return string.val.compare(string.default_val); 532 | case Table: 533 | for (auto &c : children) { 534 | if (c.differs_from_default()) { 535 | return true; 536 | } 537 | } 538 | break; 539 | default: assert(false); 540 | } 541 | 542 | return false; 543 | } 544 | 545 | // 546 | // Set the current value at every node as the node's default value 547 | // 548 | void set_defaults() 549 | { 550 | switch (type) { 551 | case Array: // XXX: Setting array defaults not supported. 552 | // You'd need to change array_item_type. 553 | break; 554 | case Boolean: data.boolean.default_val = data.boolean.val; break; 555 | case Enum: data_enum.default_val = data_enum.val; break; 556 | case Integer: data.integer.default_val = data.integer.val; break; 557 | case Number: data.number.default_val = data.number.val; break; 558 | case String: string.default_val = string.val; break; 559 | case Table: for (auto &c : children) c.set_defaults(); break; 560 | default: assert(false); 561 | } 562 | } 563 | 564 | // 565 | // Set the current value at every node to the node's default value 566 | // 567 | void reset_to_defaults() 568 | { 569 | switch (type) { 570 | case Array: // XXX: Setting array defaults not supported. 571 | // You'd need to change array_item_type. 572 | break; 573 | case Boolean: data.boolean.val = data.boolean.default_val; break; 574 | case Enum: data_enum.val = data_enum.default_val; break; 575 | case Integer: data.integer.val = data.integer.default_val; break; 576 | case Number: data.number.val = data.number.default_val; break; 577 | case String: string.val = string.default_val; break; 578 | case Table: for (auto &c : children) c.reset_to_defaults(); break; 579 | default: assert(false); 580 | } 581 | } 582 | 583 | // 584 | // Generate and return a TOML-formatted configuration for nodes that differ from their default value 585 | // 586 | // Note: Arrays of size > 0 are always considered differing 587 | // 588 | std::string generate_delta_toml(std::string path = "", bool inline_table = false, int depth = 0, bool root = true) 589 | { 590 | if (!differs_from_default()) { 591 | return ""; 592 | } 593 | 594 | if (type == Table) { 595 | std::string s = ""; 596 | bool printed_table_header = false; 597 | 598 | std::string cpath; 599 | if (path.length()) { 600 | cpath = path + "." + name; 601 | } else if (!root) { 602 | cpath = name; 603 | } 604 | 605 | if (inline_table) { 606 | if (!name.empty()) { 607 | s += name; 608 | s += " = "; 609 | } 610 | s += "{ "; 611 | } 612 | 613 | int i = 0; 614 | for (auto &c : children) { 615 | if (c.type == Table || !c.differs_from_default()) continue; 616 | 617 | if (!printed_table_header && !inline_table) { 618 | if (cpath.length()) { 619 | s += "[" + cpath + "]\n"; 620 | } 621 | printed_table_header = true; 622 | } 623 | 624 | if (inline_table && i++) s += ", "; 625 | s += c.generate_delta_toml("", inline_table, depth, false); 626 | 627 | if (!inline_table) { 628 | s += "\n"; 629 | } 630 | } 631 | 632 | if (printed_table_header) { 633 | s += "\n"; 634 | } 635 | 636 | for (auto &c : children) { 637 | if (c.type != Table || !c.differs_from_default()) continue; 638 | if (inline_table && i++) s += ", "; 639 | s += c.generate_delta_toml(cpath, inline_table, depth, false); 640 | } 641 | 642 | if (inline_table) { 643 | s += "}"; 644 | } 645 | 646 | return s; 647 | } 648 | 649 | std::string s = ""; 650 | 651 | if (name.length()) { 652 | s += string_format("%s = ", name.c_str()); 653 | } 654 | 655 | switch (type) { 656 | case Array: { 657 | s += "[\n"; 658 | int i = 0; 659 | for (auto &c : children) { 660 | if (i++) { 661 | s += ",\n"; 662 | } 663 | for (int d = 0; d < (depth+1); d++) s += " "; 664 | s += c.generate_delta_toml("", true, depth + 1, false); 665 | } 666 | s += "\n"; 667 | for (int d = 0; d < (depth+1); d++) s += " "; 668 | s += "]"; 669 | break; 670 | } 671 | case Boolean: s += data.boolean.val ? "true" : "false"; break; 672 | case Enum: { 673 | std::ostringstream oss; 674 | oss << toml::value(data_enum.values[data_enum.val]); 675 | s += oss.str(); 676 | break; 677 | } 678 | case Integer: s += string_format("%d", data.integer.val); break; 679 | case Number: s += string_format("%g", data.number.val); break; 680 | case String: { 681 | std::ostringstream oss; 682 | oss << toml::value(string.val); 683 | s += oss.str(); 684 | break; 685 | } 686 | default: assert(false); 687 | } 688 | 689 | return s; 690 | } 691 | }; 692 | 693 | // 694 | // Helpers to generate CNodes 695 | // 696 | 697 | static inline CNode ctab(std::string name, std::vector children) { 698 | return CNode(name, children); 699 | } 700 | 701 | static inline CNode carray(size_t o, size_t oc, size_t sz, std::string name, CNode item_type) { 702 | CNode node(name, Array); 703 | node.array_item_type = std::make_unique(item_type); 704 | node.serialized.offset = o; 705 | node.serialized.count_offset = oc; 706 | node.serialized.size = sz; 707 | return node; 708 | } 709 | 710 | static inline CNode cbool(size_t o, std::string name, bool val = false) { 711 | CNode node(name, Boolean); 712 | node.data.boolean = { val, val }; 713 | node.serialized.offset = o; 714 | return node; 715 | } 716 | 717 | static inline CNode cenum(size_t o, std::string name, std::vector values, std::string value) { 718 | CNode node(name, Enum); 719 | node.data_enum.values = values; 720 | int idx = node.enum_str_to_int(value); 721 | assert(idx >= 0 && "Default value invalid"); 722 | node.data_enum.val = node.data_enum.default_val = idx; 723 | node.serialized.offset = o; 724 | return node; 725 | } 726 | 727 | static inline CNode cinteger(size_t o, std::string name, int val = 0, int min = INT_MIN, int max = INT_MAX) { 728 | CNode node(name, Integer); 729 | node.data.integer = { min, max, val, val }; 730 | node.serialized.offset = o; 731 | return node; 732 | } 733 | 734 | static inline CNode cnumber(size_t o, std::string name, float val = 0, float min = -FLT_MAX, float max = +FLT_MAX) { 735 | CNode node(name, Number); 736 | node.data.number = { min, max, val, val }; 737 | node.serialized.offset = o; 738 | return node; 739 | } 740 | 741 | static inline CNode cstring(size_t o, std::string name, std::string val) { 742 | CNode node(name, String); 743 | node.string = { val, val }; 744 | node.serialized.offset = o; 745 | return node; 746 | } 747 | -------------------------------------------------------------------------------- /example/.gitignore: -------------------------------------------------------------------------------- 1 | tomlplusplus 2 | example 3 | config.h 4 | config_out.toml 5 | -------------------------------------------------------------------------------- /example/Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: all clean 2 | all: example 3 | 4 | tomlplusplus: 5 | git clone https://github.com/marzer/tomlplusplus 6 | 7 | config.h: config_spec.yml 8 | python ../gen_config.py config_spec.yml config.h 9 | 10 | example: main.cpp config.h tomlplusplus 11 | g++ -o $@ -g -std=c++17 -Itomlplusplus/include -I.. main.cpp 12 | 13 | clean: 14 | rm -f config.h example config_out.toml 15 | -------------------------------------------------------------------------------- /example/check.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import tomli 3 | with open('config_out.toml') as f: 4 | t = tomli.loads(f.read()) 5 | assert(t['company']['products'][0]['suppliers'][-1] == "Fred's Apples LLC") 6 | assert(t['company']['products'][1]['price'] == 995.75) 7 | -------------------------------------------------------------------------------- /example/config.toml: -------------------------------------------------------------------------------- 1 | [company] 2 | name = "Acme Corp" 3 | products = [ 4 | { name = "Apple", price = 1.2, inventory = 100, suppliers = [ 5 | "Midwest Orchard", 6 | "Tasty Apples Inc." 7 | ]}, 8 | { name = "TNT", price = 1000, inventory = 1000, international_shipping = false, category = "explosive"} 9 | ] 10 | 11 | [company.headquarters] 12 | state = "Arizona" 13 | city = "Phoenix" 14 | 15 | -------------------------------------------------------------------------------- /example/config_spec.yml: -------------------------------------------------------------------------------- 1 | company: 2 | name: string 3 | headquarters: 4 | state: string 5 | city: string 6 | 7 | products: 8 | type: array 9 | items: 10 | name: 11 | type: string 12 | default: New Product 13 | price: float 14 | inventory: int 15 | international_shipping: 16 | type: bool 17 | default: true 18 | suppliers: 19 | type: array 20 | items: string 21 | category: 22 | type: enum 23 | values: ['fruit', 'vegetable', 'beverage', 'explosive'] 24 | -------------------------------------------------------------------------------- /example/main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | #define DEFINE_CONFIG_TREE 6 | #include "config.h" 7 | 8 | const char *config_file_in_path = "config.toml"; 9 | const char *config_file_out_path = "config_out.toml"; 10 | 11 | void add_supplier(struct config *s, int product, const char *name) 12 | { 13 | int c = s->company.products[product].suppliers_count; 14 | s->company.products[product].suppliers = (const char **)reallocarray( 15 | s->company.products[product].suppliers, c + 1, sizeof(char *)); 16 | s->company.products[product].suppliers[c] = name; 17 | s->company.products[product].suppliers_count = c + 1; 18 | } 19 | 20 | void set_product_price(struct config *s, int product, float price) 21 | { 22 | s->company.products[product].price = price; 23 | } 24 | 25 | int main(int argc, char *argv[]) 26 | { 27 | struct config s; 28 | 29 | // Load config from user file 30 | auto toml_table = toml::parse_file(config_file_in_path); 31 | config_tree.update_from_table(toml_table); 32 | config_tree.store_to_struct(&s); 33 | 34 | // Work with config 35 | printf("Company is %s headquartered in %s, %s\n", 36 | s.company.name, 37 | s.company.headquarters.city, 38 | s.company.headquarters.state); 39 | 40 | puts("Available products:"); 41 | for (int i = 0; i < s.company.products_count; i++) { 42 | printf(" - %s %d units @ $%.2f ea.", 43 | s.company.products[i].name, 44 | s.company.products[i].inventory, 45 | s.company.products[i].price); 46 | if (s.company.products[i].international_shipping) 47 | printf(" *International*"); 48 | if (s.company.products[i].category == CONFIG_COMPANY_PRODUCTS_CATEGORY_EXPLOSIVE) 49 | printf(" *Hazardous*"); 50 | 51 | puts(""); 52 | 53 | if (s.company.products[i].suppliers_count) { 54 | puts(" Supplied by:"); 55 | for (int j = 0; j < s.company.products[i].suppliers_count; j++) 56 | printf(" - %s\n", s.company.products[i].suppliers[j]); 57 | } 58 | } 59 | 60 | // Update some config 61 | // config_tree.set_defaults(); 62 | add_supplier(&s, 0, "Fred's Apples LLC"); 63 | set_product_price(&s, 1, 995.75); 64 | 65 | // Sync config from structure 66 | config_tree.update_from_struct(&s); 67 | // config_tree.repr(); 68 | 69 | // Save config 70 | FILE *f = fopen(config_file_out_path, "wb"); 71 | assert(f != NULL); 72 | fprintf(f, "%s", config_tree.generate_delta_toml().c_str()); 73 | fclose(f); 74 | 75 | return 0; 76 | } 77 | -------------------------------------------------------------------------------- /gen_config.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # Copyright (c) 2022 Matt Borgerson 3 | # 4 | # Permission is hereby granted, free of charge, to any person obtaining a copy 5 | # of this software and associated documentation files (the "Software"), to deal 6 | # in the Software without restriction, including without limitation the rights 7 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | # copies of the Software, and to permit persons to whom the Software is 9 | # furnished to do so, subject to the following conditions: 10 | # 11 | # The above copyright notice and this permission notice shall be included in all 12 | # copies or substantial portions of the Software. 13 | # 14 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | # SOFTWARE. 21 | """ 22 | Generates C/C++ code to load/store application config based on a spec file. 23 | """ 24 | 25 | import yaml 26 | import json 27 | import logging 28 | import argparse 29 | import os 30 | 31 | from typing import Optional, Sequence, Mapping, Any 32 | 33 | 34 | logging.basicConfig(level=logging.INFO) 35 | log = logging.getLogger() 36 | 37 | 38 | class ConfigNode: 39 | def __init__(self, name: str, entry: Mapping[str, Any]): 40 | self.name: str = name 41 | 42 | # Support shorthand field: type 43 | if isinstance(entry, str): 44 | entry = { 45 | 'type': entry 46 | } 47 | 48 | self.type: str = entry.pop('type', 'table') 49 | 50 | # Type name aliases 51 | self.type = { 52 | 'bool': 'boolean', 53 | 'int': 'integer', 54 | 'float': 'number', 55 | 'str': 'string', 56 | }.get(self.type, self.type) 57 | 58 | supported_types = ('boolean', 'string', 'integer', 'number', 'enum', 'array', 'table') 59 | assert self.type in supported_types, \ 60 | f'{name}: Invalid type {self.type}, expected one of {supported_types}' 61 | 62 | self.array_item_type: 'ConfigNode' = None 63 | self.children: Sequence['ConfigNode'] = [] 64 | self.attrs: Mapping[str, Any] = {} 65 | self.value: Any = None 66 | 67 | if self.type == 'table': 68 | for k, v in entry.items(): 69 | self.children.append(ConfigNode(k, v)) 70 | else: 71 | self.attrs.update(entry) 72 | default_values = { 73 | 'boolean': False, 74 | 'string': '', 75 | 'integer': 0, 76 | 'number': 0.0, 77 | 'array': [], 78 | } 79 | 80 | self.value = entry.get('default', default_values.get(self.type, None)) 81 | if self.type == 'enum' and self.value is None: 82 | self.value = list(self.values)[0] 83 | if self.type == 'array': 84 | self.array_item_type = ConfigNode('', self.items) 85 | 86 | def __getattr__(self, name: str) -> Any: 87 | if name in self.attrs: 88 | return self.attrs[name] 89 | else: 90 | raise AttributeError(name) 91 | 92 | def __str__(self, depth: int = 0) -> str: 93 | s = ' '*depth + f'{self.name}<{self.type}>' 94 | if self.type in ('boolean', 'string', 'integer', 'number', 'enum'): 95 | s += f' = {repr(self.value)}' 96 | if self.type == 'enum': 97 | s += f" {{{', '.join(map(repr, self.values))}}}" 98 | if self.type == 'array': 99 | s += ' of {\n' 100 | s += self.array_item_type.__str__(depth=depth+1) 101 | s += ' '*depth + '}' 102 | s += '\n' 103 | for c in self.children: 104 | s += c.__str__(depth=depth+1) 105 | return s 106 | 107 | def enum_name(self, path: str) -> str: 108 | return path.replace('.', '_').upper() 109 | 110 | def enum_value(self, path: str, value: str) -> str: 111 | return self.enum_name(path + '_' + value) 112 | 113 | def enum_count(self, path: str) -> str: 114 | return self.enum_value(path, '_COUNT') 115 | 116 | def gen_c_enums(self, path: str = '') -> str: 117 | if self.name: 118 | cpath = (path + '.' + self.name) if path else self.name 119 | else: 120 | cpath = path 121 | s = '' 122 | if self.type == 'enum': 123 | s += f'enum {self.enum_name(cpath)}_ {{\n' 124 | for i, v in enumerate(self.values): 125 | s += '\t' + self.enum_value(cpath, v) + f' = {i},\n' 126 | s += '\t'+self.enum_count(cpath) + f' = {len(self.values)}\n' 127 | s += f'}};\ntypedef int {self.enum_name(cpath)};\n\n' 128 | 129 | for c in self.children: 130 | s += c.gen_c_enums(cpath) 131 | if self.array_item_type: 132 | s += self.array_item_type.gen_c_enums(cpath) 133 | return s 134 | 135 | def field_name(self, path: str) -> str: 136 | return path.replace('.', '_') 137 | 138 | def gen_c_decl(self, path: str, array=False, var='') -> str: 139 | if self.type == 'boolean': 140 | s = f'bool ' 141 | elif self.type == 'string': 142 | s = f'const char *' 143 | elif self.type == 'integer': 144 | s = f'int ' 145 | elif self.type == 'number': 146 | s = f'float ' 147 | elif self.type == 'array': 148 | s = f'struct {self.name}_item *' 149 | elif self.type == 'enum': 150 | s = f'{self.enum_name(path + "." + self.name)} ' 151 | else: 152 | assert False 153 | 154 | if array: 155 | s += '*' 156 | s += var if var else self.name 157 | return s 158 | 159 | def gen_c_struct(self, path: str = '', indent: int = 0, var: str = '', array: bool = False) -> str: 160 | if self.name: 161 | cpath = (path + '.' + self.name) if path else self.name 162 | else: 163 | cpath = path 164 | s = '' 165 | 166 | if self.array_item_type: 167 | s += self.array_item_type.gen_c_typedefs(cpath) 168 | 169 | if var: 170 | s += ' '*indent + f'struct {var} {{\n' 171 | else: 172 | s += ' '*indent + f'struct {self.name} {{\n' 173 | 174 | for c in self.children: 175 | if c.type == 'table': 176 | s += c.gen_c_struct(cpath, indent+1, var=c.name) 177 | else: 178 | if c.type == 'array': 179 | if c.array_item_type.type == 'table': 180 | s += c.array_item_type.gen_c_struct(cpath + '.' + c.name, indent=indent+1, var=c.name, array=True) 181 | else: 182 | s += ' '*(indent+1) + c.array_item_type.gen_c_decl(cpath, var=c.name, array=True) + ';\n' 183 | s += ' '*(indent+1) + 'unsigned int ' + c.name + '_count;\n' 184 | else: 185 | s += ' '*(indent+1) + c.gen_c_decl(cpath) + ';\n' 186 | 187 | if var: 188 | s += ' '*indent + '} ' + ('*' if array else '') + var + ';\n' 189 | else: 190 | s += ' '*indent + '};\n' 191 | 192 | return s 193 | 194 | 195 | class Config: 196 | def __init__(self, path: str, root_node_name: str): 197 | doc = open(path).read() 198 | doc = doc.replace('\t', ' ') ## PyYAML doesn't like tabs 199 | log.debug('Input config contents:\n%s', doc) 200 | self.root = ConfigNode(root_node_name, yaml.load(doc, yaml.Loader)) 201 | 202 | def gen_cnode_def_file(self, path: str = '', spath: str = '', node: Optional[ConfigNode] = None, depth: int = 0) -> str: 203 | if node is None: 204 | s = 'CNode {}_tree =\n'.format(self.root.name) 205 | s += self.gen_cnode_def_file(path='', spath=self.root.name, node=self.root, depth=1) 206 | s = s[0:-1] + ';\n' 207 | return s 208 | 209 | if node is self.root: 210 | cpath = '' 211 | elif node.name: 212 | cpath = (path + '.' + node.name) if path else node.name 213 | else: 214 | cpath = path 215 | 216 | s = '' 217 | ind = ' '*depth 218 | nind = ' '*(depth+1) 219 | 220 | if node.type == 'table': 221 | s += ind + f'ctab("{node.name}", {{\n' 222 | for i, c in enumerate(node.children): 223 | if i > 0: 224 | s = s[0:-1] + ',\n' 225 | s += self.gen_cnode_def_file(path=cpath, spath=spath, node=c, depth=depth+1) 226 | s += ind + '})\n' 227 | else: 228 | 229 | if node.name: 230 | offset_of_field = nind + f'offsetof(struct {spath}, {cpath}),\n' 231 | elif path == '': 232 | # Non-table array item 233 | offset_of_field = nind + '0, ' 234 | nind = '' 235 | 236 | if node.type == 'boolean': 237 | s += ind + f'cbool(\n' + offset_of_field 238 | s += nind + f'"{node.name}", {"true" if node.value else "false"})\n' 239 | elif node.type == 'string': 240 | s += ind + f'cstring(\n' + offset_of_field 241 | s += nind + f'"{node.name}", "{node.value}")\n' 242 | elif node.type == 'integer': 243 | s += ind + f'cinteger(\n' + offset_of_field 244 | s += nind + f'"{node.name}", {node.value})\n' 245 | elif node.type == 'number': 246 | s += ind + f'cnumber(\n' + offset_of_field 247 | s += nind + f'"{node.name}", {node.value})\n' 248 | elif node.type == 'array': 249 | s += ind + f'carray(\n' + offset_of_field 250 | s += nind + f'offsetof(struct {spath}, {cpath}_count),\n' 251 | if node.array_item_type.type == 'table': 252 | s += nind + f'sizeof(struct {spath}::{cpath.replace(".", "::")}),\n' 253 | else: 254 | s += nind + f'sizeof(((struct {spath} *){{0}})->{cpath}[0]),\n' 255 | s += nind + f'"{node.name}", \n' 256 | spath = spath + '::' + cpath.replace('.', '::') 257 | s += self.gen_cnode_def_file(path='', spath=spath, node=node.array_item_type, depth=depth+1) 258 | s += ind + ')\n' 259 | elif node.type == 'enum': 260 | s += ind + f'cenum(\n' + offset_of_field 261 | assert node.value, f"Missing default value for {cpath}" 262 | s += nind + f'"{node.name}", {{{", ".join(json.dumps(v) for v in node.values)}}}, {json.dumps(node.value)})\n' 263 | else: 264 | assert False, node.type 265 | 266 | return s 267 | 268 | 269 | def gen_c_file(self) -> str: 270 | return ( '#include \n' 271 | + self.root.gen_c_enums() 272 | + self.root.gen_c_struct()) 273 | 274 | 275 | def main(): 276 | ap = argparse.ArgumentParser() 277 | ap.add_argument('spec', help='Input config specification path') 278 | ap.add_argument('output', help='Output config header path') 279 | ap.add_argument('--struct', help='Name of generated config structure', required=False, default='config') 280 | args = ap.parse_args() 281 | c = Config(args.spec, args.struct) 282 | 283 | header_name = args.struct.upper() + '_H' 284 | 285 | s = f'''\ 286 | /* Autogenerated by gen_config.py do not edit */ 287 | #ifndef {header_name} 288 | #define {header_name} 289 | {c.gen_c_file()} 290 | #endif 291 | 292 | #ifdef DEFINE_CONFIG_TREE 293 | {c.gen_cnode_def_file()} 294 | #endif 295 | ''' 296 | 297 | with open(args.output, 'w') as f: 298 | f.write(s) 299 | 300 | if __name__ == '__main__': 301 | main() 302 | -------------------------------------------------------------------------------- /infographic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mborgerson/genconfig/42f85f9a2457e61d7e32542c07723565a284fcd6/infographic.png -------------------------------------------------------------------------------- /meson.build: -------------------------------------------------------------------------------- 1 | project('genconfig', 'c', version : '0.1') 2 | 3 | gen_config_script_path = files('gen_config.py') 4 | 5 | cnode_dep = declare_dependency( 6 | include_directories : include_directories('.'), 7 | sources : ['cnode.h'], 8 | ) --------------------------------------------------------------------------------