Store static strings in a dedicated pool

Because a slot id is smaller than a pointer, this change will ultimately allow reducing the slot size.
This commit is contained in:
Benoit Blanchon
2025-02-24 15:35:09 +01:00
parent 411424b74e
commit bd2dccda0e
36 changed files with 282 additions and 91 deletions

View File

@ -1,6 +1,43 @@
ArduinoJson: change log
=======================
HEAD
----
* Optimize storage of static strings
> ### BREAKING CHANGES
>
> Static string cannot contain NUL characters anymore (they could since 7.3.0).
> This is an extremely rare case, so you probably won't be affected.
>
> For example, the following code produces different output in 7.3 and 7.4:
>
> ```cpp
> JsonDocument doc;
> doc["a\0b"] = "c\0d";
> serializeJson(doc, Serial);
> // With Arduino 7.3 -> {"a\u0000b":"c\u0000d"}
> // With Arduino 7.4 -> {"a":"c"}
> ```
>
> `JsonString` contructor now only accepts two arguments, not three.
> If your code uses `JsonString` to store a string as a pointer, you must remove the size argument.
>
> For example, if you have something like this:
>
> ```cpp
> doc["key"] = JsonString(str.c_str(), str.size(), true);
> ```
>
> You must replace with either:
>
> ```cpp
> doc["key"] = JsonString(str.c_str(), true); // store as pointer, cannot contain NUL characters
> doc["key"] = JsonString(str.c_str(), str.size()); // store by copy, NUL characters allowed
> doc["key"] = str; // same as previous line for supported string classes (`String`, `std::string`, etc.)
> ```
v7.4.1 (2025-04-11)
------

View File

@ -54,7 +54,7 @@ TEST_CASE("BasicJsonDocument") {
doc["hello"] = "world";
auto copy = doc;
REQUIRE(copy.as<std::string>() == "{\"hello\":\"world\"}");
REQUIRE(allocatorLog == "AA");
REQUIRE(allocatorLog == "AAAA");
}
SECTION("capacity") {

View File

@ -275,6 +275,12 @@ inline size_t sizeofPool(
return MemoryPool<VariantData>::slotsToBytes(n);
}
inline size_t sizeofStaticStringPool(
ArduinoJson::detail::SlotCount n = ARDUINOJSON_POOL_CAPACITY) {
using namespace ArduinoJson::detail;
return MemoryPool<const char*>::slotsToBytes(n);
}
inline size_t sizeofStringBuffer(size_t iteration = 1) {
// returns 31, 63, 127, 255, etc.
auto capacity = ArduinoJson::detail::StringBuilder::initialCapacity;

View File

@ -56,6 +56,7 @@ TEST_CASE("JsonArray::add(T)") {
REQUIRE(array[0].is<int>() == false);
REQUIRE(spy.log() == AllocatorLog{
Allocate(sizeofPool()),
Allocate(sizeofStaticStringPool()),
});
}

View File

@ -104,6 +104,8 @@ TEST_CASE("deserializeJson(MemberProxy)") {
REQUIRE(err == DeserializationError::Ok);
REQUIRE(doc.as<std::string>() == "{\"hello\":\"world\",\"value\":[42]}");
REQUIRE(spy.log() == AllocatorLog{});
REQUIRE(spy.log() == AllocatorLog{
Allocate(sizeofStaticStringPool()),
});
}
}

View File

@ -825,7 +825,9 @@ TEST_CASE("shrink filter") {
deserializeJson(doc, "{}", DeserializationOption::Filter(filter));
REQUIRE(spy.log() == AllocatorLog{
Reallocate(sizeofPool(), sizeofObject(1)),
});
REQUIRE(spy.log() ==
AllocatorLog{
Reallocate(sizeofPool(), sizeofObject(1)),
Reallocate(sizeofStaticStringPool(), sizeofStaticStringPool(1)),
});
}

View File

@ -31,6 +31,7 @@ TEST_CASE("ElementProxy::add()") {
REQUIRE(doc.as<std::string>() == "[[\"world\"]]");
REQUIRE(spy.log() == AllocatorLog{
Allocate(sizeofPool()),
Allocate(sizeofStaticStringPool()),
});
}

View File

@ -25,6 +25,7 @@ TEST_CASE("MemberProxy::add()") {
REQUIRE(doc.as<std::string>() == "{\"hello\":[42]}");
REQUIRE(spy.log() == AllocatorLog{
Allocate(sizeofPool()),
Allocate(sizeofStaticStringPool()),
});
}
@ -34,6 +35,7 @@ TEST_CASE("MemberProxy::add()") {
REQUIRE(doc.as<std::string>() == "{\"hello\":[\"world\"]}");
REQUIRE(spy.log() == AllocatorLog{
Allocate(sizeofPool()),
Allocate(sizeofStaticStringPool()),
});
}
@ -44,6 +46,7 @@ TEST_CASE("MemberProxy::add()") {
REQUIRE(doc.as<std::string>() == "{\"hello\":[\"world\"]}");
REQUIRE(spy.log() == AllocatorLog{
Allocate(sizeofPool()),
Allocate(sizeofStaticStringPool()),
Allocate(sizeofString("world")),
});
}
@ -55,8 +58,8 @@ TEST_CASE("MemberProxy::add()") {
REQUIRE(doc.as<std::string>() == "{\"hello\":[\"world\"]}");
REQUIRE(spy.log() == AllocatorLog{
Allocate(sizeofPool()),
Allocate(sizeofStaticStringPool()),
Allocate(sizeofString("world")),
});
}
@ -71,6 +74,7 @@ TEST_CASE("MemberProxy::add()") {
REQUIRE(doc.as<std::string>() == "{\"hello\":[\"world\"]}");
REQUIRE(spy.log() == AllocatorLog{
Allocate(sizeofPool()),
Allocate(sizeofStaticStringPool()),
Allocate(sizeofString("world")),
});
}
@ -399,7 +403,7 @@ TEST_CASE("MemberProxy under memory constraints") {
}
SECTION("value slot allocation fails") {
timebomb.setCountdown(1);
timebomb.setCountdown(2);
// fill the pool entirely, but leave one slot for the key
doc["foo"][ARDUINOJSON_POOL_CAPACITY - 4] = 1;
@ -412,6 +416,7 @@ TEST_CASE("MemberProxy under memory constraints") {
REQUIRE(doc.overflowed() == true);
REQUIRE(spy.log() == AllocatorLog{
Allocate(sizeofPool()),
Allocate(sizeofStaticStringPool()),
AllocateFail(sizeofPool()),
});
}

View File

@ -32,6 +32,7 @@ TEST_CASE("JsonDocument::add(T)") {
REQUIRE(doc.as<std::string>() == "[\"hello\"]");
REQUIRE(spy.log() == AllocatorLog{
Allocate(sizeofPool()),
Allocate(sizeofStaticStringPool()),
});
}

View File

@ -64,6 +64,7 @@ TEST_CASE("JsonDocument constructor") {
REQUIRE(doc2.as<std::string>() == "{\"hello\":\"world\"}");
REQUIRE(spyingAllocator.log() == AllocatorLog{
Allocate(sizeofPool()),
Allocate(sizeofStaticStringPool()),
});
}
@ -87,6 +88,7 @@ TEST_CASE("JsonDocument constructor") {
REQUIRE(doc2.as<std::string>() == "[\"hello\"]");
REQUIRE(spyingAllocator.log() == AllocatorLog{
Allocate(sizeofPool()),
Allocate(sizeofStaticStringPool()),
});
}

View File

@ -22,10 +22,10 @@ TEST_CASE("JsonDocument::remove()") {
SECTION("string literal") {
doc["a"] = 1;
doc["a\0b"_s] = 2;
doc["x"] = 2;
doc["b"] = 3;
doc.remove("a\0b");
doc.remove("x");
REQUIRE(doc.as<std::string>() == "{\"a\":1,\"b\":3}");
}

View File

@ -37,7 +37,9 @@ TEST_CASE("JsonDocument::set()") {
doc.set("example");
REQUIRE(doc.as<const char*>() == "example"_s);
REQUIRE(spy.log() == AllocatorLog{});
REQUIRE(spy.log() == AllocatorLog{
Allocate(sizeofStaticStringPool()),
});
}
SECTION("const char*") {

View File

@ -75,7 +75,11 @@ TEST_CASE("JsonDocument::shrinkToFit()") {
doc.shrinkToFit();
REQUIRE(doc.as<std::string>() == "hello");
REQUIRE(spyingAllocator.log() == AllocatorLog{});
REQUIRE(spyingAllocator.log() ==
AllocatorLog{
Allocate(sizeofStaticStringPool()),
Reallocate(sizeofStaticStringPool(), sizeofStaticStringPool(1)),
});
}
SECTION("owned string") {
@ -110,7 +114,9 @@ TEST_CASE("JsonDocument::shrinkToFit()") {
REQUIRE(spyingAllocator.log() ==
AllocatorLog{
Allocate(sizeofPool()),
Allocate(sizeofStaticStringPool()),
Reallocate(sizeofPool(), sizeofObject(1)),
Reallocate(sizeofStaticStringPool(), sizeofStaticStringPool(1)),
});
}
@ -137,7 +143,9 @@ TEST_CASE("JsonDocument::shrinkToFit()") {
REQUIRE(spyingAllocator.log() ==
AllocatorLog{
Allocate(sizeofPool()),
Allocate(sizeofStaticStringPool()),
Reallocate(sizeofPool(), sizeofArray(1)),
Reallocate(sizeofStaticStringPool(), sizeofStaticStringPool(1)),
});
}
@ -164,20 +172,23 @@ TEST_CASE("JsonDocument::shrinkToFit()") {
REQUIRE(spyingAllocator.log() ==
AllocatorLog{
Allocate(sizeofPool()),
Allocate(sizeofStaticStringPool()),
Reallocate(sizeofPool(), sizeofObject(1)),
Reallocate(sizeofStaticStringPool(), sizeofStaticStringPool(2)),
});
}
SECTION("owned string in object") {
doc["key"] = "abcdefg"_s;
doc["key1"_s] = "value"_s;
doc.shrinkToFit();
REQUIRE(doc.as<std::string>() == "{\"key\":\"abcdefg\"}");
REQUIRE(doc.as<std::string>() == "{\"key1\":\"value\"}");
REQUIRE(spyingAllocator.log() ==
AllocatorLog{
Allocate(sizeofPool()),
Allocate(sizeofString("abcdefg")),
Allocate(sizeofString("key1")),
Allocate(sizeofString("value")),
Reallocate(sizeofPool(), sizeofPool(2)),
});
}

View File

@ -25,8 +25,6 @@ TEST_CASE("JsonDocument::operator[]") {
SECTION("string literal") {
REQUIRE(doc["abc"] == "ABC");
REQUIRE(cdoc["abc"] == "ABC");
REQUIRE(doc["abc\0d"] == "ABCD");
REQUIRE(cdoc["abc\0d"] == "ABCD");
}
SECTION("std::string") {
@ -114,6 +112,7 @@ TEST_CASE("JsonDocument::operator[] key storage") {
REQUIRE(doc.as<std::string>() == "{\"hello\":0}");
REQUIRE(spy.log() == AllocatorLog{
Allocate(sizeofPool()),
Allocate(sizeofStaticStringPool()),
});
}

View File

@ -26,25 +26,12 @@ TEST_CASE("JsonObject::set()") {
REQUIRE(obj2["hello"] == "world"_s);
REQUIRE(spy.log() == AllocatorLog{
Allocate(sizeofPool()),
Allocate(sizeofStaticStringPool()),
});
}
SECTION("copy local string value") {
obj1["hello"] = "world"_s;
spy.clearLog();
bool success = obj2.set(obj1);
REQUIRE(success == true);
REQUIRE(obj2["hello"] == "world"_s);
REQUIRE(spy.log() == AllocatorLog{
Allocate(sizeofPool()),
Allocate(sizeofString("world")),
});
}
SECTION("copy local key") {
obj1["hello"_s] = "world";
SECTION("copy local string key and value") {
obj1["hello"_s] = "world"_s;
spy.clearLog();
bool success = obj2.set(obj1);
@ -54,6 +41,7 @@ TEST_CASE("JsonObject::set()") {
REQUIRE(spy.log() == AllocatorLog{
Allocate(sizeofPool()),
Allocate(sizeofString("hello")),
Allocate(sizeofString("world")),
});
}
@ -110,7 +98,7 @@ TEST_CASE("JsonObject::set()") {
}
SECTION("copy fails in the middle of an array") {
TimebombAllocator timebomb(1);
TimebombAllocator timebomb(2);
JsonDocument doc3(&timebomb);
JsonObject obj3 = doc3.to<JsonObject>();

View File

@ -102,21 +102,25 @@ TEST_CASE("JsonObject::operator[]") {
REQUIRE(42 == obj[key]);
}
SECTION("should not duplicate const char*") {
SECTION("string literals") {
obj["hello"] = "world";
REQUIRE(spy.log() == AllocatorLog{Allocate(sizeofPool())});
REQUIRE(spy.log() == AllocatorLog{
Allocate(sizeofPool()),
Allocate(sizeofStaticStringPool()),
});
}
SECTION("should duplicate char* value") {
obj["hello"] = const_cast<char*>("world");
REQUIRE(spy.log() == AllocatorLog{
Allocate(sizeofPool()),
Allocate(sizeofStaticStringPool()),
Allocate(sizeofString("world")),
});
}
SECTION("should duplicate char* key") {
obj[const_cast<char*>("hello")] = "world";
obj[const_cast<char*>("hello")] = 42;
REQUIRE(spy.log() == AllocatorLog{
Allocate(sizeofPool()),
Allocate(sizeofString("hello")),
@ -136,12 +140,13 @@ TEST_CASE("JsonObject::operator[]") {
obj["hello"] = "world"_s;
REQUIRE(spy.log() == AllocatorLog{
Allocate(sizeofPool()),
Allocate(sizeofStaticStringPool()),
Allocate(sizeofString("world")),
});
}
SECTION("should duplicate std::string key") {
obj["hello"_s] = "world";
obj["hello"_s] = 42;
REQUIRE(spy.log() == AllocatorLog{
Allocate(sizeofPool()),
Allocate(sizeofString("hello")),
@ -158,7 +163,7 @@ TEST_CASE("JsonObject::operator[]") {
}
SECTION("should duplicate a non-static JsonString key") {
obj[JsonString("hello", false)] = "world";
obj[JsonString("hello", false)] = 42;
REQUIRE(spy.log() == AllocatorLog{
Allocate(sizeofPool()),
Allocate(sizeofString("hello")),
@ -166,9 +171,10 @@ TEST_CASE("JsonObject::operator[]") {
}
SECTION("should not duplicate a static JsonString key") {
obj[JsonString("hello", true)] = "world";
obj[JsonString("hello", true)] = 42;
REQUIRE(spy.log() == AllocatorLog{
Allocate(sizeofPool()),
Allocate(sizeofStaticStringPool()),
});
}

View File

@ -38,13 +38,15 @@ TEST_CASE("JsonVariant::set(JsonVariant)") {
REQUIRE(var1.as<std::string>() == "{\"value\":[42]}");
}
SECTION("stores const char* by reference") {
SECTION("stores string literals by pointer") {
var1.set("hello!!");
spyingAllocator.clearLog();
var2.set(var1);
REQUIRE(spyingAllocator.log() == AllocatorLog{});
REQUIRE(spyingAllocator.log() == AllocatorLog{
Allocate(sizeofStaticStringPool()),
});
}
SECTION("stores char* by copy") {

View File

@ -23,7 +23,9 @@ TEST_CASE("JsonVariant::set() when there is enough memory") {
REQUIRE(result == true);
CHECK(variant ==
"hello"_s); // linked string cannot contain '\0' at the moment
CHECK(spy.log() == AllocatorLog{});
CHECK(spy.log() == AllocatorLog{
Allocate(sizeofStaticStringPool()),
});
}
SECTION("const char*") {
@ -149,7 +151,9 @@ TEST_CASE("JsonVariant::set() when there is enough memory") {
REQUIRE(result == true);
REQUIRE(variant == "world"); // stores by pointer
REQUIRE(spy.log() == AllocatorLog{});
REQUIRE(spy.log() == AllocatorLog{
Allocate(sizeofStaticStringPool()),
});
}
SECTION("non-static JsonString") {
@ -265,6 +269,20 @@ TEST_CASE("JsonVariant::set() with not enough memory") {
JsonVariant v = doc.to<JsonVariant>();
SECTION("string literal") {
bool result = v.set("hello world");
REQUIRE(result == false);
REQUIRE(v.isNull());
}
SECTION("static JsonString") {
bool result = v.set(JsonString("hello world", true));
REQUIRE(result == false);
REQUIRE(v.isNull());
}
SECTION("std::string") {
bool result = v.set("hello world!!"_s);

View File

@ -57,7 +57,7 @@ TEST_CASE("JsonVariantConst::operator[]") {
SECTION("string literal") {
REQUIRE(var["ab"] == "AB"_s);
REQUIRE(var["abc"] == "ABC"_s);
REQUIRE(var["abc\0d"] == "ABCD"_s);
REQUIRE(var["abc\0d"] == "ABC"_s);
REQUIRE(var["def"].isNull());
REQUIRE(var[0].isNull());
}

View File

@ -21,7 +21,7 @@ TEST_CASE("adaptString()") {
auto s = adaptString("bravo\0alpha");
CHECK(s.isNull() == false);
CHECK(s.size() == 11);
CHECK(s.size() == 5);
CHECK(s.isStatic() == true);
}

View File

@ -104,6 +104,8 @@ TEST_CASE("deserializeMsgPack(MemberProxy)") {
REQUIRE(err == DeserializationError::Ok);
REQUIRE(doc.as<std::string>() == "{\"hello\":\"world\",\"value\":[42]}");
REQUIRE(spy.log() == AllocatorLog{});
REQUIRE(spy.log() == AllocatorLog{
Allocate(sizeofStaticStringPool()),
});
}
}

View File

@ -5,6 +5,7 @@
add_executable(ResourceManagerTests
allocVariant.cpp
clear.cpp
saveStaticString.cpp
saveString.cpp
shrinkToFit.cpp
size.cpp

View File

@ -2,7 +2,7 @@
// Copyright © 2014-2025, Benoit BLANCHON
// MIT License
#include <ArduinoJson/Memory/StringBuffer.hpp>
#include <ArduinoJson.hpp>
#include <catch.hpp>
#include "Allocators.hpp"
@ -22,7 +22,7 @@ TEST_CASE("StringBuffer") {
sb.save(&variant);
REQUIRE(variant.type() == VariantType::TinyString);
REQUIRE(variant.asString() == "hi!");
REQUIRE(variant.asString(&resources) == "hi!");
}
SECTION("Tiny string can't contain NUL") {
@ -32,7 +32,7 @@ TEST_CASE("StringBuffer") {
REQUIRE(variant.type() == VariantType::OwnedString);
auto str = variant.asString();
auto str = variant.asString(&resources);
REQUIRE(str.size() == 3);
REQUIRE(str.c_str()[0] == 'a');
REQUIRE(str.c_str()[1] == 0);
@ -45,6 +45,6 @@ TEST_CASE("StringBuffer") {
sb.save(&variant);
REQUIRE(variant.type() == VariantType::OwnedString);
REQUIRE(variant.asString() == "alfa");
REQUIRE(variant.asString(&resources) == "alfa");
}
}

View File

@ -2,7 +2,7 @@
// Copyright © 2014-2025, Benoit BLANCHON
// MIT License
#include <ArduinoJson/Memory/StringBuilder.hpp>
#include <ArduinoJson.hpp>
#include <catch.hpp>
#include "Allocators.hpp"
@ -46,7 +46,7 @@ TEST_CASE("StringBuilder") {
REQUIRE(resources.overflowed() == false);
REQUIRE(data.type() == VariantType::TinyString);
REQUIRE(data.asString() == "url");
REQUIRE(data.asString(&resources) == "url");
}
SECTION("Short string fits in first allocation") {
@ -134,9 +134,10 @@ TEST_CASE("StringBuilder::save() deduplicates strings") {
auto s2 = saveString(builder, "world");
auto s3 = saveString(builder, "hello");
REQUIRE(s1.asString() == "hello");
REQUIRE(s2.asString() == "world");
REQUIRE(+s1.asString().c_str() == +s3.asString().c_str()); // same address
REQUIRE(s1.asString(&resources) == "hello");
REQUIRE(s2.asString(&resources) == "world");
REQUIRE(+s1.asString(&resources).c_str() ==
+s3.asString(&resources).c_str()); // same address
REQUIRE(spy.log() ==
AllocatorLog{
@ -152,10 +153,10 @@ TEST_CASE("StringBuilder::save() deduplicates strings") {
auto s1 = saveString(builder, "hello world");
auto s2 = saveString(builder, "hello");
REQUIRE(s1.asString() == "hello world");
REQUIRE(s2.asString() == "hello");
REQUIRE(+s2.asString().c_str() !=
+s1.asString().c_str()); // different address
REQUIRE(s1.asString(&resources) == "hello world");
REQUIRE(s2.asString(&resources) == "hello");
REQUIRE(+s2.asString(&resources).c_str() !=
+s1.asString(&resources).c_str()); // different address
REQUIRE(spy.log() ==
AllocatorLog{
@ -170,10 +171,10 @@ TEST_CASE("StringBuilder::save() deduplicates strings") {
auto s1 = saveString(builder, "hello world");
auto s2 = saveString(builder, "worl");
REQUIRE(s1.asString() == "hello world");
REQUIRE(s2.asString() == "worl");
REQUIRE(s2.asString().c_str() !=
s1.asString().c_str()); // different address
REQUIRE(s1.asString(&resources) == "hello world");
REQUIRE(s2.asString(&resources) == "worl");
REQUIRE(s2.asString(&resources).c_str() !=
s1.asString(&resources).c_str()); // different address
REQUIRE(spy.log() ==
AllocatorLog{

View File

@ -0,0 +1,47 @@
// ArduinoJson - https://arduinojson.org
// Copyright © 2014-2025, Benoit BLANCHON
// MIT License
#include <ArduinoJson/Memory/ResourceManager.hpp>
#include <ArduinoJson/Strings/StringAdapters.hpp>
#include <catch.hpp>
#include "Allocators.hpp"
using namespace ArduinoJson::detail;
TEST_CASE("ResourceManager::saveStaticString() deduplicates strings") {
SpyingAllocator spy;
ResourceManager resources(&spy);
auto str1 = "hello";
auto str2 = "world";
auto id1 = resources.saveStaticString(str1);
auto id2 = resources.saveStaticString(str2);
REQUIRE(id1 != id2);
auto id3 = resources.saveStaticString(str1);
REQUIRE(id1 == id3);
resources.shrinkToFit();
REQUIRE(spy.log() ==
AllocatorLog{
Allocate(sizeofStaticStringPool()),
Reallocate(sizeofStaticStringPool(), sizeofStaticStringPool(2)),
});
REQUIRE(resources.overflowed() == false);
}
TEST_CASE("ResourceManager::saveStaticString() when allocation fails") {
SpyingAllocator spy(FailingAllocator::instance());
ResourceManager resources(&spy);
auto slotId = resources.saveStaticString("hello");
REQUIRE(slotId == NULL_SLOT);
REQUIRE(resources.overflowed() == true);
REQUIRE(spy.log() == AllocatorLog{
AllocateFail(sizeofStaticStringPool()),
});
}

View File

@ -34,6 +34,11 @@ class Slot {
return ptr_;
}
T& operator*() const {
ARDUINOJSON_ASSERT(ptr_ != nullptr);
return *ptr_;
}
T* operator->() const {
ARDUINOJSON_ASSERT(ptr_ != nullptr);
return ptr_;
@ -76,6 +81,14 @@ class MemoryPool {
return slots_ + id;
}
SlotId find(const T& value) const {
for (SlotId i = 0; i < usage_; i++) {
if (slots_[i] == value)
return i;
}
return NULL_SLOT;
}
void clear() {
usage_ = 0;
}

View File

@ -114,6 +114,15 @@ class MemoryPoolList {
return pools_[poolIndex].getSlot(indexInPool);
}
SlotId find(const T& value) const {
for (PoolCount i = 0; i < count_; i++) {
SlotId id = pools_[i].find(value);
if (id != NULL_SLOT)
return SlotId(i * ARDUINOJSON_POOL_CAPACITY + id);
}
return NULL_SLOT;
}
void clear(Allocator* allocator) {
for (PoolCount i = 0; i < count_; i++)
pools_[i].destroy(allocator);

View File

@ -34,6 +34,7 @@ class ResourceManager {
~ResourceManager() {
stringPool_.clear(allocator_);
variantPools_.clear(allocator_);
staticStringsPools_.clear(allocator_);
}
ResourceManager(const ResourceManager&) = delete;
@ -42,6 +43,7 @@ class ResourceManager {
friend void swap(ResourceManager& a, ResourceManager& b) {
swap(a.stringPool_, b.stringPool_);
swap(a.variantPools_, b.variantPools_);
swap(a.staticStringsPools_, b.staticStringsPools_);
swap_(a.allocator_, b.allocator_);
swap_(a.overflowed_, b.overflowed_);
}
@ -111,14 +113,34 @@ class ResourceManager {
stringPool_.dereference(s, allocator_);
}
SlotId saveStaticString(const char* s) {
auto existingSlotId = staticStringsPools_.find(s);
if (existingSlotId != NULL_SLOT)
return existingSlotId;
auto slot = staticStringsPools_.allocSlot(allocator_);
if (slot)
*slot = s;
else
overflowed_ = true;
return slot.id();
}
const char* getStaticString(SlotId id) const {
return *staticStringsPools_.getSlot(id);
}
void clear() {
variantPools_.clear(allocator_);
overflowed_ = false;
variantPools_.clear(allocator_);
stringPool_.clear(allocator_);
staticStringsPools_.clear(allocator_);
}
void shrinkToFit() {
variantPools_.shrinkToFit(allocator_);
staticStringsPools_.shrinkToFit(allocator_);
}
private:
@ -126,6 +148,7 @@ class ResourceManager {
bool overflowed_;
StringPool stringPool_;
MemoryPoolList<SlotData> variantPools_;
MemoryPoolList<const char*> staticStringsPools_;
};
ARDUINOJSON_END_PRIVATE_NAMESPACE

View File

@ -18,7 +18,7 @@ class JsonPair {
JsonPair(detail::ObjectData::iterator iterator,
detail::ResourceManager* resources) {
if (!iterator.done()) {
key_ = iterator->asString();
key_ = iterator->asString(resources);
iterator.next(resources);
value_ = JsonVariant(iterator.data(), resources);
}
@ -46,7 +46,7 @@ class JsonPairConst {
JsonPairConst(detail::ObjectData::iterator iterator,
const detail::ResourceManager* resources) {
if (!iterator.done()) {
key_ = iterator->asString();
key_ = iterator->asString(resources);
iterator.next(resources);
value_ = JsonVariantConst(iterator.data(), resources);
}

View File

@ -36,7 +36,7 @@ inline ObjectData::iterator ObjectData::findKey(
return iterator();
bool isKey = true;
for (auto it = createIterator(resources); !it.done(); it.next(resources)) {
if (isKey && stringEquals(key, adaptString(it->asString())))
if (isKey && stringEquals(key, adaptString(it->asString(resources))))
return it;
isKey = !isKey;
}

View File

@ -90,8 +90,9 @@ template <size_t N>
struct StringAdapter<const char (&)[N]> {
using AdaptedString = RamString;
static AdaptedString adapt(const char (&p)[N]) {
return RamString(p, N - 1, true);
static AdaptedString adapt(const char* p) {
ARDUINOJSON_ASSERT(p);
return RamString(p, ::strlen(p), true);
}
};

View File

@ -28,8 +28,7 @@ class JsonString {
detail::enable_if_t<detail::is_integral<TSize>::value &&
!detail::is_same<TSize, bool>::value,
int> = 0>
JsonString(const char* data, TSize sz, bool isStatic = false)
: str_(data, size_t(sz), isStatic) {}
JsonString(const char* data, TSize sz) : str_(data, size_t(sz), false) {}
// Returns a pointer to the characters.
const char* c_str() const {

View File

@ -160,7 +160,7 @@ struct Converter<const char*> : private detail::VariantAttorney {
static const char* fromJson(JsonVariantConst src) {
auto data = getData(src);
return data ? data->asString().c_str() : 0;
return data ? data->asString(getResourceManager(src)).c_str() : 0;
}
static bool checkJson(JsonVariantConst src) {
@ -178,7 +178,7 @@ struct Converter<JsonString> : private detail::VariantAttorney {
static JsonString fromJson(JsonVariantConst src) {
auto data = getData(src);
return data ? data->asString() : JsonString();
return data ? data->asString(getResourceManager(src)) : JsonString();
}
static bool checkJson(JsonVariantConst src) {

View File

@ -56,13 +56,10 @@ union VariantContent {
bool asBoolean;
uint32_t asUint32;
int32_t asInt32;
#if ARDUINOJSON_USE_EXTENSIONS
SlotId asSlotId;
#endif
ArrayData asArray;
ObjectData asObject;
CollectionData asCollection;
const char* asLinkedString;
struct StringNode* asOwnedString;
char asTinyString[tinyStringMaxLength + 1];
};

View File

@ -77,7 +77,7 @@ class VariantData {
return visit.visit(JsonString(content_.asTinyString));
case VariantType::LinkedString:
return visit.visit(JsonString(content_.asLinkedString, true));
return visit.visit(JsonString(asLinkedString(resources), true));
case VariantType::OwnedString:
return visit.visit(JsonString(content_.asOwnedString->data,
@ -216,7 +216,7 @@ class VariantData {
str = content_.asTinyString;
break;
case VariantType::LinkedString:
str = content_.asLinkedString;
str = asLinkedString(resources);
break;
case VariantType::OwnedString:
str = content_.asOwnedString->data;
@ -261,7 +261,7 @@ class VariantData {
str = content_.asTinyString;
break;
case VariantType::LinkedString:
str = content_.asLinkedString;
str = asLinkedString(resources);
break;
case VariantType::OwnedString:
str = content_.asOwnedString->data;
@ -298,12 +298,14 @@ class VariantData {
}
}
JsonString asString() const {
const char* asLinkedString(const ResourceManager* resources) const;
JsonString asString(const ResourceManager* resources) const {
switch (type_) {
case VariantType::TinyString:
return JsonString(content_.asTinyString);
case VariantType::LinkedString:
return JsonString(content_.asLinkedString, true);
return JsonString(asLinkedString(resources), true);
case VariantType::OwnedString:
return JsonString(content_.asOwnedString->data,
content_.asOwnedString->length);
@ -519,12 +521,7 @@ class VariantData {
var->setString(value, resources);
}
void setLinkedString(const char* s) {
ARDUINOJSON_ASSERT(type_ == VariantType::Null); // must call clear() first
ARDUINOJSON_ASSERT(s);
type_ = VariantType::LinkedString;
content_.asLinkedString = s;
}
bool setLinkedString(const char* s, ResourceManager* resources);
template <typename TAdaptedString>
void setTinyString(const TAdaptedString& s) {

View File

@ -18,6 +18,20 @@ inline void VariantData::setRawString(SerializedValue<T> value,
setRawString(dup);
}
inline bool VariantData::setLinkedString(const char* s,
ResourceManager* resources) {
ARDUINOJSON_ASSERT(type_ == VariantType::Null); // must call clear() first
ARDUINOJSON_ASSERT(s);
auto slotId = resources->saveStaticString(s);
if (slotId == NULL_SLOT)
return false;
type_ = VariantType::LinkedString;
content_.asSlotId = slotId;
return true;
}
template <typename TAdaptedString>
inline bool VariantData::setString(TAdaptedString value,
ResourceManager* resources) {
@ -26,10 +40,8 @@ inline bool VariantData::setString(TAdaptedString value,
if (value.isNull())
return false;
if (value.isStatic()) {
setLinkedString(value.data());
return true;
}
if (value.isStatic())
return setLinkedString(value.data(), resources);
if (isTinyString(value, value.size())) {
setTinyString(value);
@ -70,6 +82,12 @@ inline const VariantExtension* VariantData::getExtension(
}
#endif
inline const char* VariantData::asLinkedString(
const ResourceManager* resources) const {
ARDUINOJSON_ASSERT(type_ == VariantType::LinkedString);
return resources->getStaticString(content_.asSlotId);
}
template <typename T>
enable_if_t<sizeof(T) == 8, bool> VariantData::setFloat(
T value, ResourceManager* resources) {