yyjson
Skill1.1k starsChanged 35 days agoApache-2.0View on GitHub
High-performance C JSON parsing, creation, and modification with yyjson. Use when working with yyjson documents, string copy-vs-borrow ownership, dangling c_str() bugs, yyjson_alc allocators, or writer output freeing.
Trending
Quiet since July.thin dataabout the whole repository
Who's talking about LuisaGroup/LuisaCompute
- Stack Overflow“c++ error: ‘struct std::atomic_flag’ has no member named ‘test’”Stack Overflow · Dec 26, 2022 · 1 votes
- Tencent/ncnn“optimize multi-head attention (increased shared-memory tile size, add fused kernel)”GitHub · Jul 10, 2026
- bazelbuild/bazel-central-registry“wanted: [LuisaGroup/LuisaCompute]”GitHub · Nov 8, 2025
- MuGdxy/SymEigen“Try to add support for luisa-compute”GitHub · Sep 15, 2025
- LuisaGroup/LuisaRender“Remove `util/u64.h` and use `Var<ulong>` instead”GitHub · Jun 2, 2024
The biggest 5 of 5 mentions linking to the repository that our crawl found. Each line is their own headline.
In this repository22 other files it gives its agents.
- .github/copilot-instructions.mdCopilot instructions
- astSkill
- backend-architectureSkill
- cmakeSkill
- cpp-styleSkill
- debugSkill
- gitSkill
- glslangSkill
- hlslSkill
- ir-pipelineSkill
- lc_apiSkill
- lc_native_shaderSkill
- lc_optimizeSkill
- llvm-spirvSkill
- lspSkill
- project_structureSkill
- renderdocSkill
- spirv_codegenSkill
- spv-optSkill
- testSkill
- xir-passesSkill
- xmakeSkill
The file
We split this skill into the rules it gives an agent
6 rules. About 0 are common, 1 is distinctive, and 5 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈1
Distinctive · estimate
This author's own choices. Worth a look.
5
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Distinctive: line 32
Read Memory Management before adding or freeing anything: most yyjson bugs in this repo are borrowed-string lifetime bugs, not syntax bugs.
- Very specific: line 151
All of these deep-copy, string bytes included (STR/RAW values are re-allocated into the target's str_pool), so the source document may be …
- Very specific: line 273
No diagnostic, no crash at the call site — the doc keeps a pointer that dies at the end of the full expression, and you see garbage (or a …
- Very specific: line 306
YYJSON_READ_INSITU: no copy; values point into your buffer, the reader rewrites it, and the buffer must be padded with at least … - Very specific: line 309
Don't return string_views into a doc you then free.
- Very specific: line 422
All APIs null-check inputs — safe to chain: yyjson_get_str(yyjson_obj_get(root, "key")) returns NULL if missing.
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
yyjson Usage Guide
Single-header ANSI C (yyjson.h + yyjson.c). Two data models: immutable (yyjson_doc/yyjson_val) and mutable (yyjson_mut_doc/yyjson_mut_val). Free the doc to free all values. Mutable arrays/objects are intrusive circular lists (yyjson_mut_val.next); immutable ones are contiguous 16-byte slots — either way index/key lookup is a linear search, so prefer iterators.
Ownership rule of thumb: the document owns everything it allocated, and borrows every string you hand it unless you use a cpy API. See Memory Management.
Project Notes
- The bundled copy is in
src/ext/yyjson(currently 0.13.0,YYJSON_VERSION_STRINGatsrc/ext/yyjson/src/yyjson.h:590). The xmake optionlc_yyjson_use_xrepo(xmake.lua:147, used atsrc/ext/xmake.lua:36) switches to the xmake-repo package; the CMake counterpart isLUISA_COMPUTE_USE_SYSTEM_YYJSON(CMakeLists.txt:186). - The only custom
yyjson_alcin-tree ismake_allocator()insrc/xir/translators/xir2json.cpp:10-17: it forwards toluisa::detail::allocator_allocate/reallocate/deallocate(include/luisa/core/stl/memory.h:68-70— all three take an explicitalignment) with 16-byte alignment and aNULLctx. yyjson_write()/yyjson_mut_write()/yyjson_val_write()allocate their output string with the default allocator. Use the_optsvariants and pass an allocator if the output must use the same allocator as the document.- The bundled target compiles
yyjson.cwith/utf-8on MSVC (xmakelc-yyjson,src/ext/xmake.lua:47-49); the CMakeluisa-compute-ext-yyjsonobject target instead definesYYJSON_EXPORTS=1/YYJSON_IMPORTS=1(src/ext/CMakeLists.txt:464-470). - Where it is used today (the only yyjson call sites outside
src/ext/yyjson; good patterns to copy):src/ast/json2ast.cpp:2406-2430(reader + budget-limited customyyjson_alc, frees the doc on every exit),src/xir/translators/xir2json.cpp(the only builder: literal keys,_optswriter sharing one 16-byte-alignedyyjson_alc), and the readerssrc/tests/unit/xir/test_xir_translators.cpp:920-939andsrc/tests/unit/xir/test_ast_to_xir.cpp:201-207(default allocator,yyjson_doc_freeon every path). - Read Memory Management before adding or freeing anything: most yyjson bugs in this repo are borrowed-string lifetime bugs, not syntax bugs.
Read JSON
yyjson_doc *doc = yyjson_read(json, len, 0);
yyjson_doc *doc = yyjson_read_file("path", 0, NULL, &err);
yyjson_doc *doc = yyjson_read_opts((char*)dat, len, flg, alc, &err);
yyjson_doc *doc = yyjson_read_fp(fp, flg, alc, &err);
// Incremental (standard JSON only: non-standard-feature flags are ignored)
yyjson_incr_state *s = yyjson_incr_new(buf, len, flg, alc);
yyjson_doc *doc = yyjson_incr_read(s, read_len, &err);
yyjson_incr_free(s);Read flags: YYJSON_READ_NOFLAG (RFC 8259), ALLOW_COMMENTS, ALLOW_TRAILING_COMMAS, ALLOW_INF_AND_NAN, ALLOW_INVALID_UNICODE, NUMBER_AS_RAW, BIGNUM_AS_RAW (overridden by NUMBER_AS_RAW), ALLOW_BOM, ALLOW_EXT_NUMBER, ALLOW_EXT_ESCAPE, ALLOW_EXT_WHITESPACE, ALLOW_SINGLE_QUOTED_STR, ALLOW_UNQUOTED_KEY, INSITU (modifies and aliases the input buffer — it must be padded with YYJSON_PADDING_SIZE zero bytes and outlive the doc; see Reader memory), STOP_WHEN_DONE (NDJSON), JSON5.
Error: yyjson_read_err err; → err.code, err.msg, err.pos. yyjson_locate_pos(dat, len, err.pos, &line, &col, &chr) for line/col.
Access Values
yyjson_val *root = yyjson_doc_get_root(doc);
// Type checks
yyjson_is_null(v) yyjson_is_bool(v) yyjson_is_uint(v) yyjson_is_sint(v)
yyjson_is_int(v) yyjson_is_real(v) yyjson_is_num(v) yyjson_is_str(v)
yyjson_is_arr(v) yyjson_is_obj(v) yyjson_is_raw(v)
// Getters
bool b = yyjson_get_bool(v); uint64_t u = yyjson_get_uint(v);
int64_t s = yyjson_get_sint(v); int i = yyjson_get_int(v);
double d = yyjson_get_real(v); double n = yyjson_get_num(v); // any number type
const char *str = yyjson_get_str(v); size_t len = yyjson_get_len(v);
const char *raw = yyjson_get_raw(v); yyjson_type t = yyjson_get_type(v);
// Array (linear by index)
size_t sz = yyjson_arr_size(arr);
yyjson_val *v = yyjson_arr_get(arr, idx);
yyjson_val *v = yyjson_arr_get_first(arr) / yyjson_arr_get_last(arr);
// Object (linear by key)
size_t sz = yyjson_obj_size(obj);
yyjson_val *v = yyjson_obj_get(obj, "key");
yyjson_val *v = yyjson_obj_getn(obj, "key", key_len);
// String compare
yyjson_equals_str(v, "abc"); yyjson_equals_strn(v, "abc", 3);Iterate
// Array
yyjson_val *v;
yyjson_arr_iter iter = yyjson_arr_iter_with(arr);
while ((v = yyjson_arr_iter_next(&iter))) { }
// Macro:
yyjson_arr_foreach(arr, idx, max, v) { }
// Object
yyjson_val *k, *v;
yyjson_obj_iter iter = yyjson_obj_iter_with(obj);
while ((k = yyjson_obj_iter_next(&iter))) { v = yyjson_obj_iter_get_val(k); }
// Macro:
yyjson_obj_foreach(obj, idx, max, k, v) { }Mutable: use yyjson_mut_ prefix. Inside mutable iter: yyjson_mut_arr_iter_remove(&iter) / yyjson_mut_obj_iter_remove(&iter).
Create / Modify JSON
yyjson_mut_doc *doc = yyjson_mut_doc_new(alc);
yyjson_mut_val *root = yyjson_mut_obj(doc); // or yyjson_mut_arr(doc)
yyjson_mut_doc_set_root(doc, root);
// Scalars
yyjson_mut_null(doc) yyjson_mut_bool(doc, true)
yyjson_mut_uint(doc, 123) yyjson_mut_sint(doc, -123) yyjson_mut_int(doc, 123)
yyjson_mut_real(doc, 1.5) yyjson_mut_str(doc, "hello") // refs caller's string
yyjson_mut_strcpy(doc, "hello") // copies into doc
yyjson_mut_strn(doc, "hello", 5) yyjson_mut_strncpy(doc, "hello", 5) // n-variants
// Array
yyjson_mut_arr_append(arr, val) yyjson_mut_arr_prepend(arr, val)
yyjson_mut_arr_insert(arr, val, idx) yyjson_mut_arr_replace(arr, idx, val)
yyjson_mut_arr_remove(arr, idx) yyjson_mut_arr_remove_first/last(arr) yyjson_mut_arr_clear(arr)
// Convenience: yyjson_mut_arr_add_int(doc, arr, 42) / _add_str / _add_arr / _add_obj
// From C array: yyjson_mut_arr_with_sint32(doc, nums, 3)
// Object
yyjson_mut_obj_add(obj, key, val) // allows duplicates
yyjson_mut_obj_put(obj, key, val) // removes existing first (O(n))
yyjson_mut_obj_remove(obj, key) yyjson_mut_obj_clear(obj)
// Convenience: yyjson_mut_obj_add_str/int/arr/obj(doc, obj, "key", ...)
yyjson_mut_obj_remove_str(obj, "key") yyjson_mut_obj_rename_key(doc, obj, "old", "new")Important: A yyjson_mut_val can only be added to one container.
Important: yyjson_mut_str / _strn / _raw / _rawn and every object key are not
copied — only the cpy variants (yyjson_mut_strcpy, _strncpy, _rawcpy, _rawncpy,
obj_add_strcpy/obj_add_strncpy for the value only) allocate into the document. See
Memory Management before adding any dynamic string.
Immutable ↔ Mutable
yyjson_mut_doc *mdoc = yyjson_doc_mut_copy(idoc, alc); // imut -> mut document
yyjson_mut_val *mval = yyjson_val_mut_copy(mdoc, ival); // imut -> mut value (into mdoc)
yyjson_mut_doc *mdoc2 = yyjson_mut_doc_mut_copy(mdoc, alc); // mut -> mut (deep)
yyjson_mut_val *mval2 = yyjson_mut_val_mut_copy(mdoc2, mval); // mut -> mut value (into mdoc2)
yyjson_doc *idoc2 = yyjson_mut_doc_imut_copy(mdoc, alc); // mut -> imut document
yyjson_doc *idoc3 = yyjson_mut_val_imut_copy(mval, alc); // mut value -> *new document*All of these deep-copy, string bytes included (STR/RAW values are re-allocated into the
target's str_pool), so the source document may be freed as soon as the copy returns. They are
also recursive — deep trees can overflow the stack. Note the *_imut_copy results are independent
documents (own pools, free with yyjson_doc_free), not values that belong to another doc —
the *_mut_copy results are values owned by the doc passed as the first argument.
Write JSON
char *json = yyjson_write(doc, flg, &len); free(json); // default allocator only
char *json = yyjson_mut_write(mdoc, flg, &len); free(json); // default allocator only
char *json = yyjson_val_write(val, flg, &len); free(json); // default allocator only
// _opts variants allocate with the allocator you pass — release with the same one:
char *json = yyjson_mut_write_opts(mdoc, flg, &alc, &len, &err); // alc.free(alc.ctx, json)
yyjson_write_file("out.json", doc, flg, alc, &err); // allocates + frees internally
yyjson_mut_write_file("out.json", mdoc, flg, alc, &err); // allocates + frees internally
yyjson_write_fp(fp, doc, flg, alc, &err);
size_t n = yyjson_write_buf(buf, buf_len, doc, flg, &err); // no allocation at allSee Writer memory — release the string with the same allocator it was
allocated with: free() only matches a NULL / libc-backed yyjson_alc, and a custom alc must
be released via alc.free(alc.ctx, json).
Write flags: YYJSON_WRITE_NOFLAG (minified), PRETTY (4-space), PRETTY_TWO_SPACES, ESCAPE_UNICODE, ESCAPE_SLASHES, ALLOW_INF_AND_NAN, INF_AND_NAN_AS_NULL, ALLOW_INVALID_UNICODE, LOWERCASE_HEX, NEWLINE_AT_END, FP_TO_FLOAT, FP_TO_FIXED(prec) (1..15).
JSON Pointer / Patch (RFC 6901/6902/7386)
// Query (value-level variants: yyjson_ptr_get(val, ptr) / yyjson_mut_ptr_get(mval, ptr))
yyjson_val *v = yyjson_doc_ptr_get(doc, "/users/0/name");
// Modify (mutable): set/add return bool; remove returns the removed value (or NULL)
yyjson_mut_doc_ptr_set(doc, "/a", yyjson_mut_int(doc, 9)); // replaces if the key exists
yyjson_mut_doc_ptr_add(doc, "/b/-", val); // "-" appends to an array
yyjson_mut_doc_ptr_remove(doc, "/b");
// n-variants: (doc, ptr, len). x-variants add (yyjson_ptr_ctx *, yyjson_ptr_err *):
// yyjson_doc_ptr_getx(doc, ptr, len, &err)
// yyjson_mut_doc_ptr_getx(doc, ptr, len, &ctx, &err)
// yyjson_mut_doc_ptr_setx/addx(doc, ptr, len, new_val, bool create_parent, &ctx, &err)
// yyjson_mut_doc_ptr_removex(doc, ptr, len, &ctx, &err)
// ctx records the parent so yyjson_ptr_ctx_remove/append/replace can undo-edit it.
// There is also a replace family (yyjson_mut_doc_ptr_replace returns the old value).
// Patch (doc is a yyjson_mut_doc *; orig/patch are immutable values;
// all-mutable inputs: yyjson_mut_patch / yyjson_mut_merge_patch)
yyjson_mut_val *out = yyjson_patch(doc, orig, patch, &err); // RFC 6902, err: yyjson_patch_err
yyjson_mut_val *out = yyjson_merge_patch(doc, orig, patch); // RFC 7386 (recursive: stack risk)Memory Management
Everything below was checked against the bundled sources (src/ext/yyjson/src/yyjson.h,
yyjson.c, v0.13.0).
Who owns what
- The document owns everything.
yyjson_read*()gives back ayyjson_docholding all values and strings;yyjson_mut_doc_new()gives back ayyjson_mut_docholding every value created through it. Values have exactly the lifetime of their document: no per-value free, no refcounting. Release withyyjson_doc_free()/yyjson_mut_doc_free()(both no-ops onNULL) and treat everyyyjson_val *,yyjson_mut_val *, and everyconst char *returned byyyjson_get_str()/yyjson_get_raw()(mutable:yyjson_mut_get_str()/yyjson_mut_get_raw()) as dangling afterwards. - Immutable doc layout: one block containing the
yyjson_docheader followed by allyyjson_valslots (grown withalc.reallocduring the read, so don't cacheyyjson_val *across a read call; stable once it returns), plus one block holding the input-text copy (doc->str_pool).yyjson_doc_free()= at most twoalc.freecalls. - Mutable doc layout: two bump arenas —
val_pool(24-byteyyjson_mut_valslots:tag,uni,next) andstr_pool(bytes for copied strings and raw values). Chunks double in size (start0x100B / 16 values, capped at0x10000000B /0x1000000values) and nothing is returned untilyyjson_mut_doc_free().*_remove,*_iter_remove,*_clearunlink values but reclaim no memory. - Containers (array/object) are intrusive circular lists, so a
yyjson_mut_valcan live in one container only; putting it in two corrupts both. To repeat content, create a fresh value oryyjson_mut_val_mut_copy(doc, val)(recursive — deep trees can blow the stack).
Borrow vs. copy (the #1 yyjson footgun)
Non-cpy creation APIs store the caller's pointer as is: the buffer must stay alive and
unmodified for the whole lifetime of the document.
| Copies into the doc | Borrows (no copy) |
yyjson_mut_strcpy(doc, s) | yyjson_mut_str(doc, s) (needs NUL: calls strlen) |
yyjson_mut_strncpy(doc, s, n) | yyjson_mut_strn(doc, s, n) |
yyjson_mut_rawcpy / yyjson_mut_rawncpy | yyjson_mut_raw / yyjson_mut_rawn |
yyjson_mut_obj_add_strcpy(doc, o, k, v) — value only | yyjson_mut_obj_add_str (key and value) |
yyjson_mut_obj_add_strncpy(doc, o, k, v, n) — value only | yyjson_mut_obj_add_strn, plus the key k |
yyjson_mut_arr_add_strcpy / _add_strncpy | yyjson_mut_arr_add_str / _add_strn |
yyjson_mut_arr_with_strcpy / _with_strncpy | yyjson_mut_arr_with_str / _with_strn |
| — | all object keys, always: yyjson_mut_obj_add_func does key->uni.str = _key |
The one API that does copy a key is yyjson_mut_obj_rename_key / _rename_keyn ("the new_key
is copied and held by doc").
Consequences:
- There is no copy-the-key convenience. For a dynamic key build the pair yourself:
yyjson_mut_obj_add(obj, yyjson_mut_strcpy(doc, key.c_str()), value). yyjson_mut_obj_add_strncpy(doc, o, "k", sv.data(), sv.size())is the way to add aluisa::string_view/std::stringthat may embed no NUL terminator: length-delimited and copied.- Borrowed content is aliased, not snapshotted — mutating the source buffer afterwards rewrites
the JSON. Worse, "does this string need escaping?" is decided and cached in the value's tag at
creation (
YYJSON_SUBTYPE_NOESC, decided byunsafe_yyjson_is_str_noesc;unsafe_yyjson_set_strwraps it for the NUL-terminated cases):yyjson_mut_str,yyjson_mut_strcpy, the key of everyyyjson_mut_obj_add_*, and the value ofyyjson_mut_obj_add_str— so mutating a borrowed string to later contain",\or a control character emits invalid JSON instead of escaping it. Then-family (yyjson_mut_strn,yyjson_mut_strncpy,obj_add_strn,obj_add_strncpy,obj_add_strcpy) storeSUBTYPE_NONEand are re-checked at write time. Either way, don't mutate what you added. yyjson_mut_stralso requires a valid NUL terminator;str-family on an unterminated buffer is out-of-bounds reading, not just a wrong length.- Copied strings live in the document's own
str_pool,len + 1bytes (cpyhelpers write the terminator, soyyjson_get_str()works even onstrncpy-created values). To fill pool memory in place and skip the double copy,unsafe_yyjson_mut_str_alc(doc, len)returns raw space — but it does not null-terminate for you.
Never hand a borrow API a temporary
No diagnostic, no crash at the call site — the doc keeps a pointer that dies at the end of the
full expression, and you see garbage (or a crash at write/doc_free time, when the allocator
reuses the block):
// BAD: .string() is a temporary; the pointer is dead on the next line
yyjson_mut_obj_add_str(doc, root, "lightmap", cfg.output_path.filename().string().c_str());
// BAD, same class: luisa::format(...).c_str(), a local char buf[], a loop-scoped std::string
// added to an array that outlives the iteration, a std::string member written later
// GOOD: copy — the document owns the bytes
yyjson_mut_obj_add_val(doc, root, "lightmap",
yyjson_mut_strcpy(doc, cfg.output_path.filename().string().c_str()));
// GOOD: extend the source's lifetime past the document
auto name = cfg.output_path.filename().string(); // alive until end of scope
yyjson_mut_obj_add_str(doc, root, "lightmap", name.c_str());
// GOOD: length-delimited copy, no NUL requirement
yyjson_mut_obj_add_strncpy(doc, root, "lightmap", sv.data(), sv.size());Safe to borrow: string literals and other static storage (keys like "schema", "version"), and
buffers of objects known to outlive the document. Default to copying for anything computed — one
bump-arena allocation per string is cheaper than the bug. Real example:
src/xir/translators/xir2json.cpp — :40-45 borrows string literals only ("schema",
"luisa.xir.debug"), :76-78 switches to yyjson_mut_obj_add_strncpy for every computed string
(module->name(), the serialized text).
Reader memory
- Default (no
YYJSON_READ_INSITU): the whole input is copied into a doc-owned buffer (len + YYJSON_PADDING_SIZE, padded with 4 zero bytes) and escapes are unescaped in place there. Soyyjson_get_str()stays valid untilyyjson_doc_free()even if the caller's buffer is gone — this is whyconst_cast<char *>(sv.data())inyyjson_read_optsis legal. YYJSON_READ_INSITU: no copy; values point into your buffer, the reader rewrites it, and the buffer must be padded with at leastYYJSON_PADDING_SIZE(4) zero bytes and must outlive the doc. Never onconstdata, string literals, or shared/pooled memory.- Don't return
string_views into a doc you then free.src/ast/json2ast.cppis the pattern to follow: it decodes into the AST (copying what must survive) and only then callsyyjson_doc_free(document)on every exit path. - Pre-sizing:
yyjson_read_max_memory_usage(len, flg)=len * (12 + !INSITU) + 256; combine it withyyjson_alc_pool_initto read with zero malloc calls.
Writer memory
yyjson_write_opts(doc, flg, alc, &len, &err)/yyjson_mut_write_opts/yyjson_val_write_opts/yyjson_mut_val_write_optsreturn a NUL-terminated UTF-8 string allocated with thealcyou passed (lenexcludes the terminator). Free it the matching way:free()for aNULLalc, otherwisealc.free(alc.ctx, json)— seewrite_document()insrc/xir/translators/xir2json.cpp.- The non-
_optshelpers (yyjson_write,yyjson_mut_write,yyjson_val_write) always use the library's default allocator — libcmalloc/freeunlessYYJSON_CUSTOM_ALCis defined — sofree()is the only correct release; with a customyyjson_alcfor the document, output and document then live in two different heaps (that is also where mismatched free bugs come from). Use the_optsvariant to keep one allocator. yyjson_write_file/yyjson_mut_write_file/yyjson_write_fpallocate their temp buffer with the passedalc, free it before returning, and report failure viabool+yyjson_write_err(YYJSON_WRITE_ERROR_MEMORY_ALLOCATIONvs. I/O codes). Nothing to clean up at the call site.yyjson_write_buf(buf, cap, ...)andyyjson_write_number(val, buf)allocate nothing:write_bufneeds slack beyond the final JSON size (temporary space) and returns 0 if the buffer is too small;write_numberneeds a caller buffer of ≥ 21 bytes for integers and ≥ 40 bytes for reals, and returnsNULL(never overflows) on bad input.- The writer only reads the document, so borrowed strings must still be alive at write time —
writing after the source
std::strings died is exactly the dangling case above.
yyjson_alc and the repo's allocators
// Fixed pool on the stack/scratch buffer (>= 8 words). NOT thread-safe, and designed for
// one document at a time: reuse across docs with partial frees fragments it.
char buf[128 * 1024];
yyjson_alc alc; yyjson_alc_pool_init(&alc, buf, sizeof(buf));
yyjson_doc *doc = yyjson_read_opts(dat, len, 0, &alc, NULL); // NULL + MEMORY_ALLOCATION if full
// Growable pool: chunks from malloc, all released at once. NOT thread-safe.
yyjson_alc *alc = yyjson_alc_dyn_new(); // free with yyjson_alc_dyn_free() AFTER doc_free()
// Fully custom: signatures are (ctx,size), (ctx,ptr,old_size,size), (ctx,ptr)
yyjson_alc MY_ALC = { my_malloc, my_realloc, my_free, my_ctx };- Both documents and readers copy the
yyjson_alcstruct (doc->alc = *alc), so the struct itself may be a stack temporary, but everything it references — a pool's buffer, thectxobject — must outlive the document (and, for writers, the output string). NULLalc ⇒ yyjson's internal default allocator — libcmalloc/realloc/free(YYJSON_DEFAULT_ALC, a privatestaticinsrc/ext/yyjson/src/yyjson.c:2242; it becomes a null allocator underYYJSON_FREESTANDINGand can be replaced by definingYYJSON_CUSTOM_ALC).- In this repo:
xir2json.cppforwards toluisa::detail::allocator_allocate/reallocate/deallocatewith 16-byte alignment;json2ast.cppwraps libc with aYYJsonBudgetctxthat returnsNULLpastmax_parse_memory_bytes, turning a hostile/large document into a cleanYYJSON_READ_ERROR_MEMORY_ALLOCATIONinstead of unbounded memory growth — pair it withmax_document_bytes/max_string_byteschecks on the input. - Allocation failures are recoverable, not fatal: every
yyjson_mut_*creator returnsNULLand everyyyjson_mut_obj_add_*/arr_add_*returnsfalse. If unchecked, you silently lose a field rather than crash — check them when the JSON is load-bearing.
Thread safety
- Building a
yyjson_mut_docis single-threaded: arena cursors, no locks. One document per thread. yyjson_read_opts/yyjson_write_optsare thread-safe whenalcis thread-safe (orNULL) and the input data isn't modified concurrently (guaranteed unlessINSITU).- A finished
yyjson_doccan be read concurrently (yyjson_get_*, iterators are stack-local state), but never mutate values from two threads (the in-place settersyyjson_set_*/yyjson_mut_set_*, e.g.yyjson_set_str_noesc, change tags) and never read while another thread frees it. yyjson_alc_pool_initandyyjson_alc_dyn_newallocators are explicitly documented as not thread-safe.
C++ lifetime hygiene (Luisa side)
- Wrap the document:
std::unique_ptr<yyjson_mut_doc, void (*)(yyjson_mut_doc*)>(or a scope guard) instead of hand-writingyyjson_mut_doc_free()before eachreturn. Manual frees are easy to miss on early returns —src/tests/unit/xir/test_xir_translators.cpp:920-939frees on both paths, which is the fragile version. LUISA_ERROR/luisa::log_erroris[[noreturn]] noexceptand callsstd::abort()(include/luisa/core/logging.h:110,133), so it does not unwind. Nothing else throws in the yyjson users either: xmake compiles them withexceptions="no-cxx"+_HAS_EXCEPTIONS=0(scripts/xmake_func.lua:346-353— onlysrc/pyopts back in,src/py/xmake.lua:23), so a failingstd::string/luisa::format/vectorgrowth aborts instead of unwinding. The real early exits are plainreturns — keep the loop body betweendoc_newanddoc_freeallocation-light anyway (CMake/clang-cl does add/EHsc,src/CMakeLists.txt:34), or hold the doc in a guard.- Nothing in the doc needs
free()-ing individually: not values, not copied strings, not keys. Mixingfree()/luisa::detail::allocator_deallocatewith yyjson-owned memory is a bug; only the writer's returned string (andyyjson_alc_dyn_new's allocator) is ever freed by hand. yyjson_mut_doc_set_str_pool_size(doc, total_string_bytes)andyyjson_mut_doc_set_val_pool_size(doc, value_count)set the next chunk size (no immediate allocation) — worth it for large, predictable dumps (manystrcpyed strings or many values) to avoid the doubling-chunkmallocchurn (the pools grow by linking new chunks, never byrealloc-ing the old one). Note the string pool is only used by copied strings/raw values.
Number & Compile-time
// Per-value output format (mutable)
yyjson_mut_set_fp_to_float(val, true);
yyjson_mut_set_fp_to_fixed(val, 6);Compile-time defines (src/ext/yyjson/src/yyjson.h:35-133): YYJSON_DISABLE_READER,
YYJSON_DISABLE_WRITER, YYJSON_DISABLE_INCR_READER, YYJSON_DISABLE_FILE (file/fp read+write
APIs), YYJSON_DISABLE_UTILS (Pointer/Patch), YYJSON_DISABLE_FAST_FP_CONV,
YYJSON_DISABLE_NON_STANDARD, YYJSON_DISABLE_UTF8_VALIDATION,
YYJSON_DISABLE_UNALIGNED_MEMORY_ACCESS (auto-detected when left undefined),
YYJSON_READER_DEPTH_LIMIT / YYJSON_WRITER_DEPTH_LIMIT (0 = no limit), and
YYJSON_FREESTANDING (build without libc; then NULL is no longer a usable alc — pass one per
call or define YYJSON_CUSTOM_ALC). None of them is set by this repo's build files.
Null Safety
All APIs null-check inputs — safe to chain: yyjson_get_str(yyjson_obj_get(root, "key")) returns NULL if missing. For hot loops, unsafe_ prefix skips checks: unsafe_yyjson_is_str(k), unsafe_yyjson_get_uint(v).
Common Pattern: Read → Modify → Write
yyjson_doc *idoc = yyjson_read_file("in.json", 0, NULL, NULL);
yyjson_mut_doc *doc = yyjson_doc_mut_copy(idoc, NULL); // deep copy: strings are copied here
yyjson_mut_val *root = yyjson_mut_doc_get_root(doc);
yyjson_mut_val *k, *v;
yyjson_mut_obj_iter iter = yyjson_mut_obj_iter_with(root);
while ((k = yyjson_mut_obj_iter_next(&iter))) {
v = yyjson_mut_obj_iter_get_val(k);
if (yyjson_mut_is_null(v)) yyjson_mut_obj_iter_remove(&iter); // unlinks, frees nothing
}
yyjson_mut_write_file("out.json", doc, YYJSON_WRITE_PRETTY, NULL, NULL);
yyjson_doc_free(idoc);
yyjson_mut_doc_free(doc);yyjson_doc_mut_copy deep-copies, so the mutable doc is independent of idoc's memory (you may
free idoc immediately). The plain *_write calls above use the libc allocator → free(); if
the mutable doc was built with a custom yyjson_alc, use yyjson_mut_write_opts(doc, flg, &alc, …)
and alc.free(alc.ctx, json) instead.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

