Monado OpenXR Runtime
Loading...
Searching...
No Matches
u_json.hpp
Go to the documentation of this file.
1// Copyright 2021, Collabora, Ltd.
2// SPDX-License-Identifier: BSL-1.0
3/*!
4 * @file
5 * @brief C++ wrapper for cJSON.
6 * @author Mateo de Mayo <mateo.demayo@collabora.com>
7 * @ingroup aux_util
8 */
9
10#pragma once
11
12#include "util/u_file.h"
13#include "util/u_debug.h"
14#include "cjson/cJSON.h"
15
16#include <cassert>
17#include <stack>
18#include <vector>
19#include <map>
20#include <memory>
21#include <variant>
22#include <fstream>
23#include <sstream>
24
25DEBUG_GET_ONCE_LOG_OPTION(json_log, "JSON_LOG", U_LOGGING_WARN)
26
27#define JSON_TRACE(...) U_LOG_IFL_T(debug_get_log_option_json_log(), __VA_ARGS__)
28#define JSON_DEBUG(...) U_LOG_IFL_D(debug_get_log_option_json_log(), __VA_ARGS__)
29#define JSON_INFO(...) U_LOG_IFL_I(debug_get_log_option_json_log(), __VA_ARGS__)
30#define JSON_WARN(...) U_LOG_IFL_W(debug_get_log_option_json_log(), __VA_ARGS__)
31#define JSON_ERROR(...) U_LOG_IFL_E(debug_get_log_option_json_log(), __VA_ARGS__)
32#define JSON_ASSERT(fatal, predicate, ...) \
33 do { \
34 bool p = predicate; \
35 if (!p) { \
36 JSON_ERROR(__VA_ARGS__); \
37 if (fatal) { \
38 assert(p && "Assertion failed: " #predicate); \
39 exit(EXIT_FAILURE); \
40 } \
41 } \
42 } while (false);
43
44// Fatal assertion
45#define JSON_ASSERTF(predicate, ...) JSON_ASSERT(true, predicate, __VA_ARGS__)
46#define JSON_ASSERTF_(predicate) JSON_ASSERT(true, predicate, "Assertion failed " #predicate)
47
48// Warn-only assertion
49#define JSON_ASSERTW(predicate, ...) JSON_ASSERT(false, predicate, __VA_ARGS__)
50
51namespace xrt::auxiliary::util::json {
52
53using std::get;
54using std::get_if;
55using std::holds_alternative;
56using std::make_shared;
57using std::map;
58using std::shared_ptr;
59using std::string;
60using std::to_string;
61using std::variant;
62using std::vector;
63
64class JSONBuilder;
65
66/*!
67 * @brief A JSONNode wraps a cJSON object and presents useful functions for
68 * accessing the different properties of the json structure like `operator[]`,
69 * `isType()` and `asType()` methods.
70 *
71 * The main ways a user can build a JSONNode is from a json string, from a
72 * json file with `loadFromFile` or with the @ref JSONBuilder.
73 */
75{
76private:
77 friend class JSONBuilder;
78 using Ptr = shared_ptr<JSONNode>;
79
80 //! Wrapped cJSON object
81 cJSON *cjson = nullptr;
82
83 //! Whether this node is responsible for deleting the cjson object
84 bool is_owner = false;
85
86 //! Parent of this node, used only by @ref JSONBuilder.
87 JSONNode::Ptr parent = nullptr;
88
89public:
90 // Class resource management
91
92 //! This is public so that make_shared works; do not use outside of this file.
93 JSONNode(cJSON *cjson, bool is_owner, const JSONNode::Ptr &parent)
94 : cjson(cjson), is_owner(is_owner), parent(parent)
95 {
96 if (isInvalid()) {
97 const int max_length = 64;
98 string msg = string(cJSON_GetErrorPtr()).substr(0, max_length);
99 JSON_ERROR("Invalid syntax right before: '%s'", msg.c_str());
100 throw std::runtime_error(msg);
101 }
102 }
103
104
105 //! Wrap cJSON object for easy manipulation, does not take ownership
106 JSONNode(cJSON *cjson) : JSONNode(cjson, false, nullptr) {}
107
108 //! Makes a null object; `isInvalid()` on it returns true.
110
111 //! Receives a json string and constructs a wrapped cJSON object out of it.
112 JSONNode(const string &content) : JSONNode(cJSON_Parse(content.c_str()), true, nullptr) {}
113
114 JSONNode(JSONNode &&node)
115 {
116 *this = std::move(node);
117 }
118
119 JSONNode(const JSONNode &node)
120 {
121 *this = node;
122 };
123
124 JSONNode &
125 operator=(JSONNode &&rhs)
126 {
127 swap(*this, rhs);
128 return *this;
129 }
130
131 JSONNode &
132 operator=(const JSONNode &node)
133 {
134 is_owner = node.is_owner;
135 parent = node.parent;
136 if (node.is_owner) {
137 cjson = cJSON_Duplicate(node.cjson, true); // Deep copy
138 } else {
139 cjson = node.cjson; // Shallow copy
140 }
141
142 return *this;
143 };
144
145 ~JSONNode()
146 {
147 if (is_owner) {
148 cJSON_Delete(cjson);
149 }
150 }
151
152 friend void
153 swap(JSONNode &lhs, JSONNode &rhs) noexcept
154 {
155 using std::swap;
156 swap(lhs.cjson, rhs.cjson);
157 swap(lhs.is_owner, rhs.is_owner);
158 swap(lhs.parent, rhs.parent);
159 }
160
161 // Methods for explicit usage by the user of this class
162
163 static JSONNode
164 loadFromFile(const string &filepath)
165 {
166 std::ifstream file(filepath);
167 if (!file.is_open()) {
168 JSON_ERROR("Unable to open file %s", filepath.c_str());
169 return JSONNode{};
170 }
171
172 std::stringstream stream{};
173 stream << file.rdbuf();
174 string content = stream.str();
175
176 return JSONNode{content};
177 }
178
179 bool
180 saveToFile(const string &filepath) const
181 {
182 string contents = toString(false);
183 std::ofstream file(filepath);
184
185 if (!file.is_open()) {
186 JSON_ERROR("Unable to open file %s", filepath.c_str());
187 return false;
188 }
189
190 file << contents;
191 return true;
192 }
193
195 operator[](const string &key) const
196 {
197 const char *name = key.c_str();
198 JSON_ASSERTW(isObject(), "Trying to retrieve field '%s' from non-object %s", name, toString().c_str());
199
200 cJSON *value = cJSON_GetObjectItemCaseSensitive(cjson, name);
201 JSON_ASSERTW(value != nullptr, "Unable to retrieve field '%s' from %s", name, toString().c_str());
202
203 return JSONNode{value, false, nullptr};
204 }
205
207 operator[](int i) const
208 {
209 JSON_ASSERTW(isArray(), "Trying to retrieve index '%d' from non-array %s", i, toString().c_str());
210
211 cJSON *value = cJSON_GetArrayItem(cjson, i);
212 JSON_ASSERTW(value != nullptr, "Unable to retrieve index %d from %s", i, toString().c_str());
213
214 return JSONNode{value, false, nullptr};
215 }
216
217 // clang-format off
218 bool isObject() const { return isValid() && cJSON_IsObject(cjson); }
219 bool isArray() const { return isValid() && cJSON_IsArray(cjson); }
220 bool isString() const { return isValid() && cJSON_IsString(cjson); }
221 bool isNumber() const { return isValid() && cJSON_IsNumber(cjson); }
222 bool isInt() const { return isNumber() && cjson->valuedouble == cjson->valueint; }
223 bool isDouble() const { return isNumber(); }
224 bool isNull() const { return isValid() && cJSON_IsNull(cjson); }
225 bool isBool() const { return isValid() && cJSON_IsBool(cjson); }
226 bool isInvalid() const { return cjson == nullptr || cJSON_IsInvalid(cjson); }
227 bool isValid() const { return !isInvalid(); }
228
229 bool canBool() const { return isBool() || (isInt() && (cjson->valueint == 0 || cjson->valueint == 1)); }
230 // clang-format on
231
232 map<string, JSONNode>
233 asObject(const map<string, JSONNode> &otherwise = map<string, JSONNode>()) const
234 {
235 JSON_ASSERTW(isObject(), "Invalid object: %s, defaults", toString().c_str());
236 if (isObject()) {
237 map<string, JSONNode> object{};
238
239 cJSON *item = NULL;
240 cJSON_ArrayForEach(item, cjson)
241 {
242 const char *key = item->string;
243 JSON_ASSERTF(key, "Unexpected unnamed pair in json: %s", toString().c_str());
244 JSON_ASSERTW(object.count(key) == 0, "Duplicated key '%s'", key);
245 object.insert({key, JSONNode{item, false, nullptr}});
246 }
247
248 return object;
249 }
250 return otherwise;
251 }
252
253 vector<JSONNode>
254 asArray(const vector<JSONNode> &otherwise = vector<JSONNode>()) const
255 {
256 JSON_ASSERTW(isArray(), "Invalid array: %s, defaults", toString().c_str());
257 if (isArray()) {
258 vector<JSONNode> array{};
259
260 cJSON *item = NULL;
261 cJSON_ArrayForEach(item, cjson)
262 {
263 array.push_back(JSONNode{item, false, nullptr});
264 }
265
266 return array;
267 }
268 return otherwise;
269 }
270
271 string
272 asString(const string &otherwise = "") const
273 {
274 JSON_ASSERTW(isString(), "Invalid string: %s, defaults %s", toString().c_str(), otherwise.c_str());
275 return isString() ? cjson->valuestring : otherwise;
276 }
277
278 int
279 asInt(int otherwise = 0) const
280 {
281 JSON_ASSERTW(isInt(), "Invalid int: %s, defaults %d", toString().c_str(), otherwise);
282 return isInt() ? cjson->valueint : otherwise;
283 }
284
285 double
286 asDouble(double otherwise = 0.0) const
287 {
288 JSON_ASSERTW(isDouble(), "Invalid double: %s, defaults %lf", toString().c_str(), otherwise);
289 return isDouble() ? cjson->valuedouble : otherwise;
290 }
291
292 void *
293 asNull(void *otherwise = nullptr) const
294 {
295 JSON_ASSERTW(isNull(), "Invalid null: %s, defaults %p", toString().c_str(), otherwise);
296 return isNull() ? nullptr : otherwise;
297 }
298
299 bool
300 asBool(bool otherwise = false) const
301 {
302 JSON_ASSERTW(canBool(), "Invalid bool: %s, defaults %d", toString().c_str(), otherwise);
303 return isBool() ? cJSON_IsTrue(cjson) : (canBool() ? cjson->valueint : otherwise);
304 }
305
306 bool
307 hasKey(const string &key) const
308 {
309 return asObject().count(key) == 1;
310 }
311
312 string
313 toString(bool show_field = true) const
314 {
315 char *cstr = cJSON_Print(cjson);
316 string str{cstr};
317 free(cstr);
318
319 // Show the named field this comes from if any
320 if (show_field) {
321 str += "\nFrom field named: " + getName();
322 }
323
324 return str;
325 }
326
327 string
328 getName() const
329 {
330 return string(cjson->string ? cjson->string : "");
331 }
332
333 cJSON *
334 getCJSON()
335 {
336 return cjson;
337 }
338};
339
340
341/*!
342 * @brief Helper class for building cJSON trees through `operator<<`
343 *
344 * JSONBuild is implemented with a pushdown automata to keep track of the JSON
345 * construction state.
346 *
347 */
349{
350private:
351 enum class StackAlphabet
352 {
353 Base, // Unique occurrence as the base of the stack
354 Array,
355 Object
356 };
357
358 enum class State
359 {
360 Empty,
361 BuildArray,
362 BuildObjectKey,
363 BuildObjectValue,
364 Finish,
365 Invalid
366 };
367
368 enum class InputAlphabet
369 {
370 StartArray,
371 EndArray,
372 StartObject,
373 EndObject,
374 PushKey,
375 PushValue,
376 };
377
378 using G = StackAlphabet;
379 using S = InputAlphabet;
380 using Q = State;
381
382 std::stack<StackAlphabet> stack{{G::Base}};
383 State state{Q::Empty};
384 JSONNode::Ptr node = nullptr; //!< Current node we are pointing to in the tree.
385
386 using JSONValue = variant<string, const char *, int, double, bool>;
387
388 //! String representation of @p value.
389 static string
390 valueToString(const JSONValue &value)
391 {
392 string s = "JSONValue<invalid>()";
393 if (const string *v = get_if<string>(&value)) {
394 s = string{"JSONValue<string>("} + *v + ")";
395 } else if (const char *const *v = get_if<const char *>(&value)) {
396 s = string{"JSONValue<const char*>("} + *v + ")";
397 } else if (const int *v = get_if<int>(&value)) {
398 s = string{"JSONValue<int>("} + to_string(*v) + ")";
399 } else if (const double *v = get_if<double>(&value)) {
400 s = string{"JSONValue<double>("} + to_string(*v) + ")";
401 } else if (const bool *v = get_if<bool>(&value)) {
402 s = string{"JSONValue<bool>("} + to_string(*v) + ")";
403 } else {
404 JSON_ASSERTF(false, "Unsupported variant type");
405 s = "[Invalid JSONValue]";
406 }
407 return s;
408 }
409
410 //! Construct a cJSON object out of native types.
411 static cJSON *
412 makeCJSONValue(const JSONValue &value)
413 {
414 cJSON *ret = nullptr;
415 if (holds_alternative<string>(value)) {
416 ret = cJSON_CreateString(get<string>(value).c_str());
417 } else if (holds_alternative<const char *>(value)) {
418 ret = cJSON_CreateString(get<const char *>(value));
419 } else if (holds_alternative<int>(value)) {
420 ret = cJSON_CreateNumber(get<int>(value));
421 } else if (holds_alternative<double>(value)) {
422 ret = cJSON_CreateNumber(get<double>(value));
423 } else if (holds_alternative<bool>(value)) {
424 ret = cJSON_CreateBool(get<bool>(value));
425 } else {
426 JSON_ASSERTF(false, "Unexpected value");
427 }
428 return ret;
429 }
430
431 /*!
432 * @brief Receives inputs and transitions the automata from state to state.
433 *
434 * This is the table of transitions. Can be thought of as three regular FSM
435 * that get switched based on the stack's [top] value. The function is
436 * the implementation of the table.
437 *
438 * [top], [state], [symbol] -> [new-state], [stack-action]
439 * Base, Empty, PushValue -> Finish, -
440 * Base, Empty, StartObject -> BuildObjectKey, push(Object)
441 * Base, Empty, StartArray -> BuildArray, push(Array)
442 * Array, BuildArray, PushValue -> BuildArray, -
443 * Array, BuildArray, StartArray -> BuildArray, push(Array)
444 * Array, BuildArray, EndArray -> [1], pop
445 * Array, BuildArray, StartObject -> BuildObjectKey, push(Object)
446 * Object, BuildObjectKey, PushKey -> BuildObjectValue, -
447 * Object, BuildObjectKey, EndObject -> [1], pop
448 * Object, BuildObjectValue, PushValue -> BuildObjectKey, -
449 * Object, BuildObjectValue, StartObject -> BuildObjectKey, push(Object)
450 * Object, BuildObjectValue, StartArray -> BuildArray, push(Array)
451 * _, _, _, -> Invalid, -
452 *
453 * [1]: Empty or BuildArray or BuildObjectKey depending on new stack.top
454 */
455 void
456 transition(InputAlphabet symbol, const JSONValue &value)
457 {
458 StackAlphabet top = stack.top();
459 JSON_DEBUG("stacksz=%zu top=%d state=%d symbol=%d value=%s", stack.size(), static_cast<int>(top),
460 static_cast<int>(state), static_cast<int>(symbol), valueToString(value).c_str());
461
462 // This is basically an if-defined transition function for a pushdown automata
463 if (top == G::Base && state == Q::Empty && symbol == S::PushValue) {
464
465 JSON_ASSERTF(node == nullptr, "Failed with %s", valueToString(value).c_str());
466 cJSON *cjson_value = makeCJSONValue(value);
467 node = make_shared<JSONNode>(cjson_value, true, nullptr);
468
469 state = Q::Finish;
470
471 } else if (top == G::Base && state == Q::Empty && symbol == S::StartObject) {
472
473 JSON_ASSERTF(node == nullptr, "Failed with %s", valueToString(value).c_str());
474 cJSON *cjson_object = cJSON_CreateObject();
475 node = make_shared<JSONNode>(cjson_object, true, nullptr);
476
477 state = Q::BuildObjectKey;
478 stack.push(G::Object);
479
480 } else if (top == G::Base && state == Q::Empty && symbol == S::StartArray) {
481
482 JSON_ASSERTF(node == nullptr, "Failed with %s", valueToString(value).c_str());
483 cJSON *cjson_array = cJSON_CreateArray();
484 node = make_shared<JSONNode>(cjson_array, true, nullptr);
485
486 state = Q::BuildArray;
487 stack.push(G::Array);
488
489 } else if (top == G::Array && state == Q::BuildArray && symbol == S::PushValue) {
490
491 JSON_ASSERTF(node->cjson != nullptr, "Failed with %s", valueToString(value).c_str());
492 cJSON *cjson_value = makeCJSONValue(value);
493 cJSON_AddItemToArray(node->cjson, cjson_value);
494 // node = node; // The current node does not change, it is still the array
495
496 state = Q::BuildArray;
497
498 } else if (top == G::Array && state == Q::BuildArray && symbol == S::StartArray) {
499
500 JSON_ASSERTF(node->cjson != nullptr, "Failed with %s", valueToString(value).c_str());
501 cJSON *cjson_array = cJSON_CreateArray();
502 cJSON_AddItemToArray(node->cjson, cjson_array);
503 node = make_shared<JSONNode>(cjson_array, false, node);
504
505 state = Q::BuildArray;
506 stack.push(G::Array);
507
508 } else if (top == G::Array && state == Q::BuildArray && symbol == S::EndArray) {
509
510 stack.pop();
511 map<G, Q> m{{G::Object, Q::BuildObjectKey}, {G::Array, Q::BuildArray}, {G::Base, Q::Finish}};
512 state = m[stack.top()];
513
514 JSON_ASSERTF(node->cjson != nullptr, "Failed with %s", valueToString(value).c_str());
515 if (node->parent) {
516 node = node->parent;
517 } else {
518 JSON_ASSERTF(stack.top() == G::Base, "Unexpected non-root node without");
519 }
520
521 } else if (top == G::Array && state == Q::BuildArray && symbol == S::StartObject) {
522
523 JSON_ASSERTF(node->cjson != nullptr, "Failed with %s", valueToString(value).c_str());
524 cJSON *cjson_object = cJSON_CreateObject();
525 cJSON_AddItemToArray(node->cjson, cjson_object);
526 node = make_shared<JSONNode>(cjson_object, false, node);
527
528 state = Q::BuildObjectKey;
529 stack.push(G::Object);
530
531 } else if (top == G::Object && state == Q::BuildObjectKey && symbol == S::PushKey) {
532
533 JSON_ASSERTF(node->cjson != nullptr, "Failed with %s", valueToString(value).c_str());
534 JSON_ASSERTF(holds_alternative<string>(value), "Non-string key not allowed");
535 cJSON *cjson_null = cJSON_CreateNull();
536 cJSON_AddItemToObject(node->cjson, get<string>(value).c_str(), cjson_null);
537 node = make_shared<JSONNode>(cjson_null, false, node);
538
539 state = Q::BuildObjectValue;
540
541 } else if (top == G::Object && state == Q::BuildObjectKey && symbol == S::EndObject) {
542
543 stack.pop();
544 map<G, Q> m{{G::Object, Q::BuildObjectKey}, {G::Array, Q::BuildArray}, {G::Base, Q::Finish}};
545 state = m[stack.top()];
546
547 JSON_ASSERTF(node->cjson != nullptr, "Failed with %s", valueToString(value).c_str());
548 if (node->parent) {
549 node = node->parent;
550 } else {
551 JSON_ASSERTF(stack.top() == G::Base, "Unexpected non-root node without")
552 }
553
554 } else if (top == G::Object && state == Q::BuildObjectValue && symbol == S::PushValue) {
555
556 JSON_ASSERTF(node->cjson != nullptr, "Failed with %s", valueToString(value).c_str());
557 JSON_ASSERTF(cJSON_IsNull(node->cjson), "Partial pair value is not null");
558 cJSON *cjson_value = makeCJSONValue(value);
559 cJSON_ReplaceItemInObject(node->parent->cjson, node->cjson->string, cjson_value);
560 node->cjson = cjson_value;
561 node = node->parent;
562
563 state = Q::BuildObjectKey;
564
565 } else if (top == G::Object && state == Q::BuildObjectValue && symbol == S::StartObject) {
566
567 JSON_ASSERTF(node->cjson != nullptr, "Failed with %s", valueToString(value).c_str());
568 JSON_ASSERTF(cJSON_IsNull(node->cjson), "Partial pair value is not null");
569 cJSON *cjson_object = cJSON_CreateObject();
570 cJSON_ReplaceItemInObject(node->parent->cjson, node->cjson->string, cjson_object);
571 node->cjson = cjson_object;
572
573 state = Q::BuildObjectKey;
574 stack.push(G::Object);
575
576 } else if (top == G::Object && state == Q::BuildObjectValue && symbol == S::StartArray) {
577
578 JSON_ASSERTF(node->cjson != nullptr, "Failed with %s", valueToString(value).c_str());
579 JSON_ASSERTF(cJSON_IsNull(node->cjson), "Partial pair value is not null");
580 cJSON *cjson_array = cJSON_CreateArray();
581 cJSON_ReplaceItemInObject(node->parent->cjson, node->cjson->string, cjson_array);
582 node->cjson = cjson_array;
583
584 state = Q::BuildArray;
585 stack.push(G::Array);
586
587 } else {
588
589 JSON_ASSERTF(false, "Invalid construction transition: top=%d state=%d symbol=%d value=%s",
590 static_cast<int>(top), static_cast<int>(state), static_cast<int>(symbol),
591 valueToString(value).c_str());
592 node = make_shared<JSONNode>();
593
594 state = Q::Invalid;
595 }
596
597 JSON_DEBUG("After transition: node=%p parent=%p\n", (void *)node.get(),
598 (void *)(node ? node->parent.get() : nullptr));
599 }
600
601public:
602 JSONBuilder() {}
603
604 //! Receives "[", "]", "{", "}", or any of string, const char*, double, int,
605 //! bool as inputs. Updates the JSONBuilder state with it, after finishing the
606 //! JSON tree, obtain the result with @ref getBuiltNode.
608 operator<<(const JSONValue &value)
609 {
610 bool is_string = holds_alternative<string>(value) || holds_alternative<const char *>(value);
611 if (!is_string) {
612 transition(S::PushValue, value);
613 return *this;
614 }
615
616 string as_string = holds_alternative<string>(value) ? get<string>(value) : get<const char *>(value);
617 if (as_string == "[") {
618 transition(S::StartArray, as_string);
619 } else if (as_string == "]") {
620 transition(S::EndArray, as_string);
621 } else if (as_string == "{") {
622 transition(S::StartObject, as_string);
623 } else if (as_string == "}") {
624 transition(S::EndObject, as_string);
625 } else if (state == Q::BuildObjectKey) {
626 transition(S::PushKey, as_string);
627 } else if (state == Q::BuildObjectValue) {
628 transition(S::PushValue, as_string);
629 } else {
630 JSON_ASSERTF(false, "Invalid state=%d value=%s", static_cast<int>(state), as_string.c_str());
631 }
632 return *this;
633 }
634
635 //! Gets the built JSONNode or crash if the construction has not finished
636 JSONNode::Ptr
638 {
639 JSON_ASSERTF(state == Q::Finish, "Trying to getBuiltNode but the construction has not ended");
640 return node;
641 }
642};
643
644} // namespace xrt::auxiliary::util::json
Helper class for building cJSON trees through operator<<
Definition u_json.hpp:349
JSONBuilder & operator<<(const JSONValue &value)
Receives "[", "]", "{", "}", or any of string, const char*, double, int, bool as inputs.
Definition u_json.hpp:608
JSONNode::Ptr getBuiltNode()
Gets the built JSONNode or crash if the construction has not finished.
Definition u_json.hpp:637
A JSONNode wraps a cJSON object and presents useful functions for accessing the different properties ...
Definition u_json.hpp:75
JSONNode(cJSON *cjson)
Wrap cJSON object for easy manipulation, does not take ownership.
Definition u_json.hpp:106
JSONNode()
Makes a null object; isInvalid() on it returns true.
Definition u_json.hpp:109
JSONNode(cJSON *cjson, bool is_owner, const JSONNode::Ptr &parent)
This is public so that make_shared works; do not use outside of this file.
Definition u_json.hpp:93
JSONNode(const string &content)
Receives a json string and constructs a wrapped cJSON object out of it.
Definition u_json.hpp:112
@ U_LOGGING_WARN
Warning messages: indicating a potential problem.
Definition u_logging.h:49
Small debug helpers.
Very simple file opening functions.