├── .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 | 
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 | )
--------------------------------------------------------------------------------