Subclass json_object so primitives shrink from 168 to 24 bytes

The previous "every member in a struct" layout cost 168 bytes per
json_object, even for JSON_NULL / JSON_TRUE / JSON_FALSE nodes that
have no payload. Splitting json_object into a small base class plus
json_number / json_string / json_array / json_hash subclasses brings
each instance down to just the size of its actual contents:

  json_object (base, TRUE / FALSE / NULL)   24 bytes
  json_number                                48 bytes
  json_string                                48 bytes
  json_array  (empty)                        48 bytes
  json_hash   (empty)                        72 bytes

Other size wins along the way:

* Drop enable_shared_from_this<json_object> (its embedded weak_ptr
  was 16 bytes per node). json_pull now keeps an explicit
  container_stack and the parser no longer needs to resurrect a
  shared_ptr from a raw `parent` walk.
* Remove the unused `refcon` slot from the string variant.
* No virtual destructor: shared_ptr keeps the deleter from the
  original std::make_shared<json_xxx> call, so destroying a
  shared_ptr<json_object> still runs the right subclass dtor.

The base class exposes type-tagged accessors (o->string(),
o->number(), o->array(), o->keys(), o->values(), o->large_signed(),
o->large_unsigned()) that assert the type matches and downcast to
the appropriate subclass storage. All call sites were swept from
the old `o->value.X.Y` field paths to these accessors. A raw-pointer
overload of json_hash_get() replaces the few external uses of
shared_from_this() that survived in geojson-loop.cpp.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Erica Fischer
2026-05-30 17:49:08 -07:00
co-authored by Cursor
parent 3da03c6075
commit f366b2c4aa
14 changed files with 424 additions and 291 deletions
+112 -83
View File
@@ -86,22 +86,47 @@ static inline int read_wrap(json_pull *j) {
return c;
}
static json_object_ptr fabricate_object(json_pull *jp, json_object *parent, json_type type) {
auto o = std::make_shared<json_object>();
o->type = type;
o->parent = parent;
o->parser = jp;
// Construct an instance of the right subclass for the given type.
// JSON_TRUE / JSON_FALSE / JSON_NULL and the parse-token types are bare
// json_objects; the value-bearing types each get their own subclass.
static json_object_ptr make_object(json_type type, json_object *parent, json_pull *jp) {
json_object_ptr o;
switch (type) {
case JSON_NUMBER:
o = std::make_shared<json_number>(parent, jp);
break;
case JSON_STRING:
o = std::make_shared<json_string>(parent, jp);
break;
case JSON_ARRAY:
o = std::make_shared<json_array>(parent, jp);
break;
case JSON_HASH:
o = std::make_shared<json_hash>(parent, jp);
break;
default:
o = std::make_shared<json_object>(type, parent, jp);
break;
}
return o;
}
static json_object_ptr fabricate_object(json_pull *jp, json_object *parent, json_type type) {
return make_object(type, parent, jp);
}
static inline json_object *current_container(json_pull *j) {
return j->container_stack.empty() ? nullptr : j->container_stack.back().get();
}
static json_object_ptr add_object(json_pull *j, json_type type) {
json_object *c = j->container.get();
json_object_ptr o = fabricate_object(j, c, type);
json_object *c = current_container(j);
json_object_ptr o = make_object(type, c, j);
if (c != nullptr) {
if (c->type == JSON_ARRAY) {
if (c->expect == JSON_ITEM) {
c->value.array.array.push_back(o);
c->array().push_back(o);
c->expect = JSON_COMMA;
} else {
j->error = "Expected a comma, not a list item";
@@ -109,7 +134,7 @@ static json_object_ptr add_object(json_pull *j, json_type type) {
}
} else if (c->type == JSON_HASH) {
if (c->expect == JSON_VALUE) {
c->value.object.values.back() = o;
c->values().back() = o;
c->expect = JSON_COMMA;
} else if (c->expect == JSON_KEY) {
if (type != JSON_STRING) {
@@ -117,8 +142,8 @@ static json_object_ptr add_object(json_pull *j, json_type type) {
return nullptr;
}
c->value.object.keys.push_back(o);
c->value.object.values.push_back(nullptr);
c->keys().push_back(o);
c->values().push_back(nullptr);
c->expect = JSON_COLON;
} else {
j->error = "Expected a comma or colon";
@@ -134,16 +159,18 @@ static json_object_ptr add_object(json_pull *j, json_type type) {
return o;
}
json_object_ptr json_hash_get(json_object_ptr o, const char *s) {
json_object_ptr json_hash_get(json_object *o, const char *s) {
if (o == nullptr || o->type != JSON_HASH) {
return nullptr;
}
for (size_t i = 0; i < o->value.object.keys.size(); i++) {
const auto &key = o->value.object.keys[i];
const auto &keys = o->keys();
const auto &vals = o->values();
for (size_t i = 0; i < keys.size(); i++) {
const auto &key = keys[i];
if (key != nullptr && key->type == JSON_STRING) {
if (key->value.string.string == s) {
return o->value.object.values[i];
if (key->string() == s) {
return vals[i];
}
}
}
@@ -151,19 +178,23 @@ json_object_ptr json_hash_get(json_object_ptr o, const char *s) {
return nullptr;
}
json_object_ptr json_hash_get(json_object_ptr o, const char *s) {
return json_hash_get(o.get(), s);
}
json_object_ptr json_read_separators(json_pull_ptr jp, json_separator_callback cb, void *state) {
int c;
json_pull *j = jp.get();
// In case there is an error at the top level
if (j->container == nullptr) {
if (j->container_stack.empty()) {
j->root.reset();
}
again:
c = read_wrap(j);
if (c == EOF) {
if (j->container != nullptr) {
if (!j->container_stack.empty()) {
j->error = "Reached EOF without all containers being closed";
}
@@ -204,8 +235,8 @@ again:
if (o == nullptr) {
return nullptr;
}
j->container = o;
j->container->expect = JSON_ITEM;
o->expect = JSON_ITEM;
j->container_stack.push_back(o);
if (cb != nullptr) {
cb(JSON_ARRAY, j, state);
@@ -215,32 +246,26 @@ again:
}
case ']': {
if (j->container == nullptr) {
json_object *cc = current_container(j);
if (cc == nullptr) {
j->error = "Found ] at top level";
return nullptr;
}
if (j->container->type != JSON_ARRAY) {
if (cc->type != JSON_ARRAY) {
j->error = "Found ] not in an array";
return nullptr;
}
if (j->container->expect != JSON_COMMA) {
if (!(j->container->expect == JSON_ITEM && j->container->value.array.array.size() == 0)) {
if (cc->expect != JSON_COMMA) {
if (!(cc->expect == JSON_ITEM && cc->array().size() == 0)) {
j->error = "Found ] without final element";
return nullptr;
}
}
json_object_ptr ret = j->container;
// Walk up to the parent container. The parent (if any) still owns
// `ret` via its own array vector, so the raw `parent` pointer is
// still valid and we can resurrect a shared_ptr to it.
if (ret->parent != nullptr) {
j->container = ret->parent->shared_from_this();
} else {
j->container.reset();
}
json_object_ptr ret = j->container_stack.back();
j->container_stack.pop_back();
return ret;
}
@@ -251,8 +276,8 @@ again:
if (o == nullptr) {
return nullptr;
}
j->container = o;
j->container->expect = JSON_KEY;
o->expect = JSON_KEY;
j->container_stack.push_back(o);
if (cb != nullptr) {
cb(JSON_HASH, j, state);
@@ -262,29 +287,26 @@ again:
}
case '}': {
if (j->container == nullptr) {
json_object *cc = current_container(j);
if (cc == nullptr) {
j->error = "Found } at top level";
return nullptr;
}
if (j->container->type != JSON_HASH) {
if (cc->type != JSON_HASH) {
j->error = "Found } not in a hash";
return nullptr;
}
if (j->container->expect != JSON_COMMA) {
if (!(j->container->expect == JSON_KEY && j->container->value.object.keys.size() == 0)) {
if (cc->expect != JSON_COMMA) {
if (!(cc->expect == JSON_KEY && cc->keys().size() == 0)) {
j->error = "Found } without final element";
return nullptr;
}
}
json_object_ptr ret = j->container;
if (ret->parent != nullptr) {
j->container = ret->parent->shared_from_this();
} else {
j->container.reset();
}
json_object_ptr ret = j->container_stack.back();
j->container_stack.pop_back();
return ret;
}
@@ -350,16 +372,17 @@ again:
/////////////////////////// Comma
case ',': {
if (j->container != nullptr) {
if (j->container->expect != JSON_COMMA) {
json_object *cc = current_container(j);
if (cc != nullptr) {
if (cc->expect != JSON_COMMA) {
j->error = "Found unexpected comma";
return nullptr;
}
if (j->container->type == JSON_HASH) {
j->container->expect = JSON_KEY;
if (cc->type == JSON_HASH) {
cc->expect = JSON_KEY;
} else {
j->container->expect = JSON_ITEM;
cc->expect = JSON_ITEM;
}
}
@@ -373,17 +396,18 @@ again:
/////////////////////////// Colon
case ':': {
if (j->container == nullptr) {
json_object *cc = current_container(j);
if (cc == nullptr) {
j->error = "Found colon at top level";
return nullptr;
}
if (j->container->expect != JSON_COLON) {
if (cc->expect != JSON_COLON) {
j->error = "Found unexpected colon";
return nullptr;
}
j->container->expect = JSON_VALUE;
cc->expect = JSON_VALUE;
if (cb != nullptr) {
cb(JSON_COLON, j, state);
@@ -463,27 +487,27 @@ again:
json_object_ptr n = add_object(j, JSON_NUMBER);
if (n != nullptr) {
n->value.number.number = atof(j->number_buffer.c_str());
n->value.number.large_signed = 0;
n->value.number.large_unsigned = 0;
n->number() = atof(j->number_buffer.c_str());
n->large_signed() = 0;
n->large_unsigned() = 0;
#define MAX_SAFE_INTEGER 9007199254740991.0
#define MIN_SAFE_INTEGER -9007199254740991.0
if (!decimal && n->value.number.number > MAX_SAFE_INTEGER) {
if (!decimal && n->number() > MAX_SAFE_INTEGER) {
errno = 0;
char *err = nullptr;
unsigned long long ull = strtoull(j->number_buffer.c_str(), &err, 10);
if (errno == 0 && (err == nullptr || *err == '\0')) {
n->value.number.large_unsigned = ull;
n->large_unsigned() = ull;
}
}
if (!decimal && n->value.number.number < MIN_SAFE_INTEGER) {
if (!decimal && n->number() < MIN_SAFE_INTEGER) {
errno = 0;
char *err = nullptr;
long long ll = strtoll(j->number_buffer.c_str(), &err, 10);
if (errno == 0 && (err == nullptr || *err == '\0')) {
n->value.number.large_signed = ll;
n->large_signed() = ll;
}
}
}
@@ -614,8 +638,7 @@ again:
json_object_ptr s = add_object(j, JSON_STRING);
if (s != nullptr) {
s->value.string.string = std::move(val);
s->value.string.refcon = nullptr;
s->string() = std::move(val);
}
return s;
}
@@ -653,13 +676,16 @@ static void clear_back_pointers(json_object *o) {
}
if (o->type == JSON_HASH) {
for (size_t i = 0; i < o->value.object.keys.size(); i++) {
clear_back_pointers(o->value.object.keys[i].get());
clear_back_pointers(o->value.object.values[i].get());
const auto &keys = o->keys();
const auto &vals = o->values();
for (size_t i = 0; i < keys.size(); i++) {
clear_back_pointers(keys[i].get());
clear_back_pointers(vals[i].get());
}
} else if (o->type == JSON_ARRAY) {
for (size_t i = 0; i < o->value.array.array.size(); i++) {
clear_back_pointers(o->value.array.array[i].get());
const auto &arr = o->array();
for (size_t i = 0; i < arr.size(); i++) {
clear_back_pointers(arr[i].get());
}
}
@@ -679,7 +705,7 @@ void json_disconnect(json_object_ptr o) {
json_object *parent = o->parent;
if (parent != nullptr) {
if (parent->type == JSON_ARRAY) {
auto &arr = parent->value.array.array;
auto &arr = parent->array();
for (size_t i = 0; i < arr.size(); i++) {
if (arr[i].get() == o.get()) {
arr.erase(arr.begin() + i);
@@ -687,8 +713,8 @@ void json_disconnect(json_object_ptr o) {
}
}
} else if (parent->type == JSON_HASH) {
auto &keys = parent->value.object.keys;
auto &vals = parent->value.object.values;
auto &keys = parent->keys();
auto &vals = parent->values();
for (size_t i = 0; i < keys.size(); i++) {
if (keys[i].get() == o.get()) {
@@ -739,7 +765,7 @@ static void json_print_one(std::string &val, json_object *o) {
} else if (o->type == JSON_STRING) {
string_append_c(val, '\"');
for (const char *cp = o->value.string.string.c_str(); *cp != '\0'; cp++) {
for (const char *cp = o->string().c_str(); *cp != '\0'; cp++) {
if (*cp == '\\' || *cp == '"') {
string_append_c(val, '\\');
string_append_c(val, *cp);
@@ -756,16 +782,16 @@ static void json_print_one(std::string &val, json_object *o) {
string_append_c(val, '\"');
} else if (o->type == JSON_NUMBER) {
if (o->value.number.large_signed != 0) {
if (o->large_signed() != 0) {
char s[65];
snprintf(s, sizeof(s), "%lld", o->value.number.large_signed);
snprintf(s, sizeof(s), "%lld", o->large_signed());
string_append(val, s);
} else if (o->value.number.large_unsigned != 0) {
} else if (o->large_unsigned() != 0) {
char s[65];
snprintf(s, sizeof(s), "%llu", o->value.number.large_unsigned);
snprintf(s, sizeof(s), "%llu", o->large_unsigned());
string_append(val, s);
} else {
char *s = dtoa_milo(o->value.number.number);
char *s = dtoa_milo(o->number());
string_append(val, s);
free(s);
}
@@ -789,20 +815,23 @@ static void json_print(std::string &val, json_object *o) {
} else if (o->type == JSON_HASH) {
string_append_c(val, '{');
for (size_t i = 0; i < o->value.object.keys.size(); i++) {
json_print(val, o->value.object.keys[i].get());
const auto &keys = o->keys();
const auto &vals = o->values();
for (size_t i = 0; i < keys.size(); i++) {
json_print(val, keys[i].get());
string_append_c(val, ':');
json_print(val, o->value.object.values[i].get());
if (i + 1 < o->value.object.keys.size()) {
json_print(val, vals[i].get());
if (i + 1 < keys.size()) {
string_append_c(val, ',');
}
}
string_append_c(val, '}');
} else if (o->type == JSON_ARRAY) {
string_append_c(val, '[');
for (size_t i = 0; i < o->value.array.array.size(); i++) {
json_print(val, o->value.array.array[i].get());
if (i + 1 < o->value.array.array.size()) {
const auto &arr = o->array();
for (size_t i = 0; i < arr.size(); i++) {
json_print(val, arr[i].get());
if (i + 1 < arr.size()) {
string_append_c(val, ',');
}
}
+143 -33
View File
@@ -3,6 +3,7 @@
#include <stdio.h>
#include <sys/types.h>
#include <cassert>
#include <memory>
#include <string>
#include <vector>
@@ -33,48 +34,152 @@ struct json_pull;
typedef std::shared_ptr<json_object> json_object_ptr;
typedef std::shared_ptr<json_pull> json_pull_ptr;
// json_object owns its descendants via std::shared_ptr in std::vector<>s,
// and keeps raw back-pointers to its parent and to the parser. The back-pointers
// remain valid as long as the node is attached to the tree (the parent is kept
// alive by holding a shared_ptr to this child, and the parser is kept alive by
// the caller's json_pull_ptr). json_disconnect() splices a node out of its
// parent and clears those back-pointers in the detached subtree, so the
// detached subtree can outlive the original parser.
// json_object is a small base type that just records the JSON type and
// the back-pointers to its parent and parser. The actual value payload
// lives in a type-specific subclass (json_number, json_string, json_array,
// json_hash), so that JSON_TRUE / JSON_FALSE / JSON_NULL nodes pay only
// the base-class cost and a JSON_HASH does not also drag along a string
// or a number field. Type-tagged accessor methods on the base class
// downcast and return references to the underlying subclass storage.
//
// Children are owned by their parent (via std::vector<json_object_ptr>
// inside json_array / json_hash); the raw `parent` and `parser`
// back-pointers stay valid as long as the node is attached to the tree.
// json_disconnect() splices a node out of its parent and walks the
// detached subtree clearing those back-pointers so the subtree can
// outlive the original parser.
//
// json_object intentionally has no virtual functions and no virtual
// destructor: subclasses are constructed via std::make_shared<json_xxx>(),
// and std::shared_ptr remembers the deleter from the original type, so
// destroying a shared_ptr<json_object> that actually points at a
// json_string still runs ~json_string(). Dispatch on `type` is what the
// rest of the code already does. The accessor methods assert at debug
// time that the type matches before downcasting.
struct json_object : public std::enable_shared_from_this<json_object> {
struct json_object {
json_object *parent = nullptr;
json_pull *parser = nullptr;
json_type type = JSON_NULL;
int expect = 0;
json_type type;
int expect = 0; // used by the parser on JSON_ARRAY / JSON_HASH nodes
// Members named to match the previous C union layout so that existing
// access paths like `o->value.string.string` and `o->value.array.array[i]`
// continue to work. This is no longer a union because std::string and
// std::vector have non-trivial destructors.
struct value_t {
struct {
double number = 0;
unsigned long long large_unsigned = 0;
long long large_signed = 0;
} number;
json_object(json_type t) : type(t) {}
json_object(json_type t, json_object *p, json_pull *pl) : parent(p), parser(pl), type(t) {}
struct {
std::string string;
void *refcon = nullptr; // reference constant for caller's use
} string;
// Type-tagged accessors. Each one asserts that the receiver is of
// the right kind, then downcasts to the storage in the appropriate
// subclass. Inline so the assert and cast disappear at -O.
inline std::string &string();
inline const std::string &string() const;
struct {
std::vector<json_object_ptr> array;
} array;
inline double &number();
inline double number() const;
inline unsigned long long &large_unsigned();
inline unsigned long long large_unsigned() const;
inline long long &large_signed();
inline long long large_signed() const;
struct {
std::vector<json_object_ptr> keys;
std::vector<json_object_ptr> values;
} object;
} value;
inline std::vector<json_object_ptr> &array();
inline const std::vector<json_object_ptr> &array() const;
inline std::vector<json_object_ptr> &keys();
inline const std::vector<json_object_ptr> &keys() const;
inline std::vector<json_object_ptr> &values();
inline const std::vector<json_object_ptr> &values() const;
};
struct json_number : json_object {
double number_value = 0;
unsigned long long large_unsigned_value = 0;
long long large_signed_value = 0;
json_number() : json_object(JSON_NUMBER) {}
json_number(json_object *p, json_pull *pl) : json_object(JSON_NUMBER, p, pl) {}
};
struct json_string : json_object {
std::string string_value;
json_string() : json_object(JSON_STRING) {}
json_string(json_object *p, json_pull *pl) : json_object(JSON_STRING, p, pl) {}
};
struct json_array : json_object {
std::vector<json_object_ptr> array_value;
json_array() : json_object(JSON_ARRAY) {}
json_array(json_object *p, json_pull *pl) : json_object(JSON_ARRAY, p, pl) {}
};
struct json_hash : json_object {
std::vector<json_object_ptr> keys_value;
std::vector<json_object_ptr> values_value;
json_hash() : json_object(JSON_HASH) {}
json_hash(json_object *p, json_pull *pl) : json_object(JSON_HASH, p, pl) {}
};
inline std::string &json_object::string() {
assert(type == JSON_STRING);
return static_cast<json_string *>(this)->string_value;
}
inline const std::string &json_object::string() const {
assert(type == JSON_STRING);
return static_cast<const json_string *>(this)->string_value;
}
inline double &json_object::number() {
assert(type == JSON_NUMBER);
return static_cast<json_number *>(this)->number_value;
}
inline double json_object::number() const {
assert(type == JSON_NUMBER);
return static_cast<const json_number *>(this)->number_value;
}
inline unsigned long long &json_object::large_unsigned() {
assert(type == JSON_NUMBER);
return static_cast<json_number *>(this)->large_unsigned_value;
}
inline unsigned long long json_object::large_unsigned() const {
assert(type == JSON_NUMBER);
return static_cast<const json_number *>(this)->large_unsigned_value;
}
inline long long &json_object::large_signed() {
assert(type == JSON_NUMBER);
return static_cast<json_number *>(this)->large_signed_value;
}
inline long long json_object::large_signed() const {
assert(type == JSON_NUMBER);
return static_cast<const json_number *>(this)->large_signed_value;
}
inline std::vector<json_object_ptr> &json_object::array() {
assert(type == JSON_ARRAY);
return static_cast<json_array *>(this)->array_value;
}
inline const std::vector<json_object_ptr> &json_object::array() const {
assert(type == JSON_ARRAY);
return static_cast<const json_array *>(this)->array_value;
}
inline std::vector<json_object_ptr> &json_object::keys() {
assert(type == JSON_HASH);
return static_cast<json_hash *>(this)->keys_value;
}
inline const std::vector<json_object_ptr> &json_object::keys() const {
assert(type == JSON_HASH);
return static_cast<const json_hash *>(this)->keys_value;
}
inline std::vector<json_object_ptr> &json_object::values() {
assert(type == JSON_HASH);
return static_cast<json_hash *>(this)->values_value;
}
inline const std::vector<json_object_ptr> &json_object::values() const {
assert(type == JSON_HASH);
return static_cast<const json_hash *>(this)->values_value;
}
struct json_pull {
const char *error = nullptr; // points at a string literal; no allocation
int line = 1;
@@ -85,7 +190,11 @@ struct json_pull {
ssize_t buffer_tail = 0;
ssize_t buffer_head = 0;
json_object_ptr container;
// Stack of currently-open containers; the top is the innermost container
// being parsed. Replaces the previous single `container` pointer / parent
// walk, which previously required enable_shared_from_this<json_object>
// on every json_object instance (16 extra bytes per node).
std::vector<json_object_ptr> container_stack;
json_object_ptr root;
std::string number_buffer;
@@ -118,6 +227,7 @@ void json_free(json_object_ptr &j);
void json_disconnect(json_object_ptr j);
json_object_ptr json_hash_get(json_object_ptr o, const char *s);
json_object_ptr json_hash_get(json_object *o, const char *s);
std::string json_stringify(json_object_ptr o);