Compare commits

...
Author SHA1 Message Date
Niels Lohmann f13fbd2577 Add benchmarks for parsing from a FILE* and for many values from a stream
ParseFILE/* parses the nativejson-benchmark files from a FILE*, and
ParseStreamValues/* reads 10,000 small values from one std::istringstream
with operator>> (NDJSON objects and plain numbers), which measures the
per-value cost of stream input.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-10-14 17:34:01 +02:00
Niels Lohmann c391abccb1 Lock a FILE once per parse instead of once per character
Parsing from a FILE* reads one character at a time with std::fgetc, which
locks and unlocks the FILE object for every character. file_input_adapter
now locks the file once when it is created and unlocks it when it is
destroyed - also when parsing throws - and reads with the unlocked stdio
functions in between: flockfile/getc_unlocked/funlockfile with glibc,
Apple, Android, and the BSDs, and _lock_file/_fgetc_nolock/_unlock_file
with the Microsoft C runtime. Other C libraries keep std::fgetc. The lock
is recursive, so fread() in the binary readers and stdio calls of a SAX
handler or parser callback on the same thread keep working.

On an Apple M1 Max, parsing from a FILE* is 2.9 to 4.0 times as fast
(twitter.json 14.3 -> 4.1 ms, citm_catalog.json 35.4 -> 8.8 ms); with
glibc, whose locking is cheaper, 1.1 to 1.3 times. The values and the
position of the file after parsing are unchanged.

Other threads that use the same FILE object now wait until parsing is
done instead of interleaving their reads with the parser's.
JSON_DISABLE_UNLOCKED_FILE_IO restores std::fgetc, for programs in which a
SAX handler waits for another thread that uses the same FILE. The new
tests check from a second thread that the lock is released after success
and after a parse error, and run again with the macro.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-10-14 17:34:00 +02:00
Niels Lohmann d5631dccbd Scan the get area of an input stream in bulk
Parsing from a std::istream read one character at a time through the
streambuf, so none of the parser's fast paths applied: it was 1.8 to 2.5
times slower than reading the file into a string and parsing that.

input_stream_adapter now exposes the streambuf's get area (gptr() to
egptr()) to the lexer as a window. The protected get-area members are
reached through member pointers formed in a class derived from
std::streambuf, which is ordinary, standard-conforming member access. The
lexer scans the window with the same bulk paths as a string, and asks
the streambuf for the next character as before when the window is used
up. Nothing is read ahead, copied, or put back, so the stream is left
exactly where it was left before, with and without
JSON_PRECISE_STREAM_POSITION, and a streambuf without a get area is still
read one character at a time.

The adapter also keeps the current token for error messages as a pointer
into the get area, and copies it out only before the get area is
refilled, instead of the lexer copying every character.

On an Apple M1 Max (Apple Clang, -O3), parsing from an std::ifstream is
1.6 to 2.1 times as fast (twitter.json 4.08 -> 2.30 ms, citm_catalog.json
8.75 -> 4.08 ms), and reading 100,000 small values with operator>> 1.4
(objects) to 4.0 (integers) times as fast. With GCC 13 and libstdc++ on
x86-64 Linux, std::ifstream is 1.5 to 2.2 times as fast. Parsing from a
stream is now within 11 to 18 % of reading the file into a string first.

JSON_DISABLE_STREAM_BULK_SCAN restores reading one character at a time,
for streambufs that do not follow the usual get-area contract. The new
tests compare every observable result (values, exception messages, stream
state, remaining characters) for get areas of 1 to 4096 bytes against a
streambuf without a get area, and run again with the macro and with
JSON_PRECISE_STREAM_POSITION.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-10-14 17:26:38 +02:00
Niels Lohmann 80d00eda50 Let the lexer's bulk fast paths run on a window of the input
The SWAR string scan, the bulk number scan, and the indentation skip were
only enabled for adapters that support seeking (contiguous random-access
input), because they rely on the lazy token and position tracking that
reconstructs both from the consumed input on demand.

This decouples the bulk paths from seeking, so that an adapter can expose
a contiguous block that is only a window into its input (bulk_window):

- bulk_consume() does the bookkeeping that get() would have done for the
  skipped characters when token and position are tracked eagerly.
- A number that reaches the end of a window is finished character by
  character, because it may continue after the window.
- After a bulk-scanned number, a window adapter reads the terminating
  character and ungets it, as scan_number() does, so that the input
  handed back to the user is consumed exactly as before.
- An adapter can keep the current token itself (supports_token_tracking,
  begin_token(), mark_token_start(), copy_token()) so the lexer need not
  copy every character for its error messages.

No adapter declares any of this yet, so the behavior is unchanged.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-10-14 17:13:58 +02:00
22 changed files with 2096 additions and 60 deletions

No files matched your search

@@ -132,6 +132,10 @@ outside of a string, invalid) byte; see the [FAQ entry](../../home/faq.md#nul-by
- [operator>>](../operator_gtgt.md) - deserialize from stream
- [`JSON_STRICT_NUL_HANDLING`](../macros/json_strict_nul_handling.md) - opt in to rejecting a NUL byte in the input
instead of treating it as end of input
- [`JSON_DISABLE_STREAM_BULK_SCAN`](../macros/json_disable_stream_bulk_scan.md) - read a `std::istream` one character
at a time
- [`JSON_DISABLE_UNLOCKED_FILE_IO`](../macros/json_disable_unlocked_file_io.md) - read a `FILE*` with the locking stdio
functions
## Version history
@@ -143,6 +147,10 @@ outside of a string, invalid) byte; see the [FAQ entry](../../home/faq.md#nul-by
- Extended overload (2) to accept heterogeneous iterator+sentinel pairs (C++20 ranges support) in version 3.13.0.
- `JSON_STRICT_NUL_HANDLING` added in version 3.13.0 to optionally reject a NUL byte in the input instead of treating
it as end of input; planned to become the default in version 4.0.0.
- Parsing from an `std::istream` scans the characters in its stream buffer in bulk in version 3.13.0
([`JSON_DISABLE_STREAM_BULK_SCAN`](../macros/json_disable_stream_bulk_scan.md)).
- Parsing from a `FILE*` locks the file once instead of once per character in version 3.13.0
([`JSON_DISABLE_UNLOCKED_FILE_IO`](../macros/json_disable_unlocked_file_io.md)).
!!! warning "Deprecation"
+19
View File
@@ -112,6 +112,16 @@ By default, a `'\0'` (NUL) byte anywhere in the input is treated as end of input
outside of a string, invalid) byte; see the [FAQ entry](../../home/faq.md#nul-bytes-in-the-input) for details and the
[`JSON_STRICT_NUL_HANDLING`](../macros/json_strict_nul_handling.md) macro to opt into rejecting it instead.
!!! tip "Performance"
Contiguous inputs (strings, string views, vectors, arrays, pointer ranges) are the fastest to parse. A
`#!cpp std::istream` comes close: the parser scans the characters its stream buffer holds in bulk, without reading
ahead, so the stream is left exactly where it would be after reading one character at a time. A `#!cpp FILE*` is
read one character at a time, but the file is locked only once, so other threads that use the same `FILE` object
wait until parsing is done. See [input sources and performance](../../features/parsing/index.md#input-sources-and-performance),
[`JSON_DISABLE_STREAM_BULK_SCAN`](../macros/json_disable_stream_bulk_scan.md), and
[`JSON_DISABLE_UNLOCKED_FILE_IO`](../macros/json_disable_unlocked_file_io.md).
## Examples
??? example "Example: (1) parse from a character array"
@@ -247,6 +257,10 @@ outside of a string, invalid) byte; see the [FAQ entry](../../home/faq.md#nul-by
- [operator>>](../operator_gtgt.md) - deserialize from stream
- [`JSON_STRICT_NUL_HANDLING`](../macros/json_strict_nul_handling.md) - opt in to rejecting a NUL byte in the input
instead of treating it as end of input
- [`JSON_DISABLE_STREAM_BULK_SCAN`](../macros/json_disable_stream_bulk_scan.md) - read a `std::istream` one character
at a time
- [`JSON_DISABLE_UNLOCKED_FILE_IO`](../macros/json_disable_unlocked_file_io.md) - read a `FILE*` with the locking stdio
functions
## Version history
@@ -261,6 +275,11 @@ outside of a string, invalid) byte; see the [FAQ entry](../../home/faq.md#nul-by
it as end of input; planned to become the default in version 4.0.0.
- Extended empty-input detection to also cover an `std::istream` without a stream buffer, and fixed a crash
(`std::terminate`) when parsing from an `std::istream` with `eofbit` in its exception mask, in version 3.13.0.
- Parsing from an `std::istream` scans the characters in its stream buffer in bulk in version 3.13.0
([`JSON_DISABLE_STREAM_BULK_SCAN`](../macros/json_disable_stream_bulk_scan.md) restores reading one character at a
time).
- Parsing from a `FILE*` locks the file once instead of once per character in version 3.13.0
([`JSON_DISABLE_UNLOCKED_FILE_IO`](../macros/json_disable_unlocked_file_io.md) restores `std::fgetc`).
!!! warning "Deprecation"
@@ -116,6 +116,10 @@ consumer `sax` has a super-linear complexity.
A UTF-8 byte order mark is silently ignored.
When reading from a `#!c FILE*`, the file is locked while it is parsed. The lock is recursive, so the SAX handler may
use the same file with stdio functions, but it must not wait for another thread that uses the same `FILE` object; see
[`JSON_DISABLE_UNLOCKED_FILE_IO`](../macros/json_disable_unlocked_file_io.md).
## Examples
??? example
@@ -137,6 +141,10 @@ A UTF-8 byte order mark is silently ignored.
- [parse](parse.md) - deserialize from a compatible input
- [accept](accept.md) - check if the input is valid JSON
- [`JSON_DISABLE_STREAM_BULK_SCAN`](../macros/json_disable_stream_bulk_scan.md) - read a `std::istream` one character
at a time
- [`JSON_DISABLE_UNLOCKED_FILE_IO`](../macros/json_disable_unlocked_file_io.md) - read a `FILE*` with the locking stdio
functions
## Version history
@@ -148,6 +156,10 @@ A UTF-8 byte order mark is silently ignored.
- Extended overload (2) to accept heterogeneous iterator+sentinel pairs (C++20 ranges support) in version 3.13.0.
- `JSON_PRECISE_STREAM_POSITION` added in version 3.13.0 to optionally leave a `#!cpp std::istream` positioned right
after the parsed value when `strict` is `#!cpp false`.
- Parsing from an `std::istream` scans the characters in its stream buffer in bulk in version 3.13.0
([`JSON_DISABLE_STREAM_BULK_SCAN`](../macros/json_disable_stream_bulk_scan.md)).
- Parsing from a `FILE*` locks the file once instead of once per character in version 3.13.0
([`JSON_DISABLE_UNLOCKED_FILE_IO`](../macros/json_disable_unlocked_file_io.md)).
!!! warning "Deprecation"
+4
View File
@@ -16,6 +16,10 @@ header. See also the [macro overview page](../../features/macros.md).
## Parsing
- [**JSON_DISABLE_STREAM_BULK_SCAN**](json_disable_stream_bulk_scan.md) - read a `std::istream` one character at a time
instead of scanning its stream buffer in bulk
- [**JSON_DISABLE_UNLOCKED_FILE_IO**](json_disable_unlocked_file_io.md) - read a `FILE*` with `std::fgetc` instead of
locking it once and using the unlocked stdio functions
- [**JSON_PRECISE_STREAM_POSITION**](json_precise_stream_position.md) - opt in to leaving an input stream positioned
right after a parsed number
- [**JSON_STRICT_BINARY_UTF8**](json_strict_binary_utf8.md) - opt in to checking strings for valid UTF-8 in the CBOR,
@@ -0,0 +1,145 @@
# JSON_DISABLE_STREAM_BULK_SCAN
```cpp
#define JSON_DISABLE_STREAM_BULK_SCAN /* value */
```
When defined to `1`, parsing from a `#!cpp std::istream` reads the input one character at a time through the stream
buffer, as in version 3.12.0 and earlier, instead of scanning the characters that the stream buffer already holds in
bulk.
The macro is an escape hatch for stream buffers that do not follow the usual contract of `std::basic_streambuf` (see
the requirements on the stream buffer in the notes below). It does not change any result: values, exceptions, and the
position of the stream after parsing are the same either way. Only the speed differs.
## Default definition
The default value is `0`: streams are scanned in bulk.
```cpp
#define JSON_DISABLE_STREAM_BULK_SCAN 0
```
## Notes
!!! note "How streams are scanned"
A `std::streambuf` keeps the part of the input it has read but not yet handed out in its *get area*, the range
from `gptr()` to `egptr()`. A `std::filebuf`, for instance, reads the file in blocks of a few kilobytes into its get
area, and a `std::stringbuf` exposes its whole string as one get area.
When parsing from a `std::istream`, the parser scans this get area directly, with the same fast paths that it uses
for a `std::string`: runs of ordinary string characters, numbers, and indentation are recognized several bytes at a
time instead of being requested from the stream buffer one character at a time. When the parser reaches the end of
the get area, it asks the stream buffer for the next character as before, which makes the stream buffer refill its
get area.
The parser only reads characters that it would also have read one at a time, and it advances the stream buffer
exactly over the characters it consumes. Nothing is read ahead, buffered elsewhere, or put back. Therefore, the
stream is left exactly where it was left before:
- [`operator>>`](../operator_gtgt.md) and [`sax_parse`](../basic_json/sax_parse.md) with `strict = false` can still
read several values from one stream, and the stream can still be used with other `std::istream` functions after a
value has been read.
- With [`JSON_PRECISE_STREAM_POSITION`](json_precise_stream_position.md), the stream is still positioned right after
a parsed number.
- Exception messages, including the line, the column, and the last read token, are unchanged.
!!! note "Performance"
Parsing from a `std::ifstream` or `std::istringstream` is typically 1.5 to 2 times as fast as before, and reading
many small values from one stream with `operator>>` 1.4 to 4 times as fast. Parsing from a stream is then within
about 10 to 20 % of first reading the whole file into a `std::string` and parsing that.
A stream buffer without a get area (one that only overrides `underflow()` and `uflow()` and hands out every
character from there) offers nothing to scan in bulk, so it is read one character at a time as before.
!!! note "Requirements on the stream buffer"
The bulk scan relies on the following behavior, which every stream buffer of the standard library and every stream
buffer that keeps its input in its get area has:
- When `sgetc()` returns a character while `gptr() < egptr()`, that character is `*gptr()`; when `sbumpc()` returns
one, it was at `gptr()` before the call.
- Characters in the get area are not changed while they are in the get area.
- `underflow()` or `uflow()` either sets up a get area that holds the returned character at `gptr()` (or at
`gptr() - 1` after `uflow()`), or leaves the get area empty.
A stream buffer that returns characters from `uflow()` while leaving unrelated characters in its get area, or that
changes the characters in its get area in place, does not meet these requirements. Define the macro to `1` for
such stream buffers.
!!! warning "Define consistently"
The macro selects between two definitions of the library's internal stream input adapter. It must therefore be
defined identically for **every** translation unit that includes the library; mixing translation units that define
it with ones that do not is an ODR violation. Prefer setting it as a compile definition on the target rather than
with `#!cpp #define` in individual source files.
!!! note "Other inputs"
Strings, string views, vectors, arrays, and pointer or iterator ranges over contiguous memory are scanned in bulk
anyway. Reading from a `#!cpp FILE*` is not affected by this macro; see
[`JSON_DISABLE_UNLOCKED_FILE_IO`](json_disable_unlocked_file_io.md) for that input.
## Examples
??? example "Example: reading values from a stream (default)"
The stream is scanned in bulk, and the values are read one after the other:
```cpp
#include <iostream>
#include <sstream>
#include <nlohmann/json.hpp>
using json = nlohmann::json;
int main()
{
std::istringstream input(R"({"id": 1} {"id": 2} 42 rest)");
json j1, j2, j3;
input >> j1 >> j2 >> j3; // {"id":1}, {"id":2}, 42
std::string word;
input >> word; // "rest"
}
```
??? example "Example: reading one character at a time (macro defined to 1)"
With the macro, the same program reads the stream one character at a time, with the same results:
```cpp
#define JSON_DISABLE_STREAM_BULK_SCAN 1
#include <iostream>
#include <sstream>
#include <nlohmann/json.hpp>
using json = nlohmann::json;
int main()
{
std::istringstream input(R"({"id": 1} {"id": 2} 42 rest)");
json j1, j2, j3;
input >> j1 >> j2 >> j3; // {"id":1}, {"id":2}, 42
std::string word;
input >> word; // "rest"
}
```
## See also
- [**parse**](../basic_json/parse.md) - deserialize from a compatible input
- [**operator>>**](../operator_gtgt.md) - deserialize from stream
- [**sax_parse**](../basic_json/sax_parse.md) - generate SAX events
- [**JSON_PRECISE_STREAM_POSITION**](json_precise_stream_position.md) - leave an input stream positioned right after a
parsed number
- [**JSON_DISABLE_UNLOCKED_FILE_IO**](json_disable_unlocked_file_io.md) - read a `FILE*` with the locking stdio
functions
- [Parsing: input sources and performance](../../features/parsing/index.md#input-sources-and-performance)
## Version history
- Added in version 3.13.0.
@@ -0,0 +1,113 @@
# JSON_DISABLE_UNLOCKED_FILE_IO
```cpp
#define JSON_DISABLE_UNLOCKED_FILE_IO /* value */
```
When defined to `1`, parsing from a `#!cpp FILE*` reads every character with `std::fgetc`, as in version 3.12.0 and
earlier, instead of locking the file once and reading it with the unlocked stdio functions.
It does not change any result: values, exceptions, and the position of the file after parsing are the same either way.
Only the speed and the locking of the file differ (see the notes on locking below).
## Default definition
The default value is `0`: the file is locked once and read with the unlocked stdio functions where they are available.
```cpp
#define JSON_DISABLE_UNLOCKED_FILE_IO 0
```
## Notes
!!! note "Background"
The C standard library makes every `std::fgetc` call thread-safe by locking and unlocking the `FILE` object. When
parsing from a `FILE*`, the parser reads one character at a time, so this happens for every character of the input.
Instead, the parser locks the file once when parsing starts and unlocks it when parsing ends - also when an
exception is thrown - and reads the characters in between without locking:
| C library | functions |
|------------------------------------------------------------|----------------------------------------------------|
| glibc (Linux), Apple (macOS, iOS), Android, FreeBSD, NetBSD, OpenBSD, DragonFly BSD | `flockfile`, `getc_unlocked`, `funlockfile` (POSIX) |
| Microsoft (MSVC, clang-cl) | `_lock_file`, `_fgetc_nolock`, `_unlock_file` |
| others (e.g., musl, MinGW, newlib) | `std::fgetc`, as before |
This makes parsing from a `FILE*` 3 to 4 times as fast with the Apple C library and 1.1 to 1.3 times as fast with
glibc, whose locking is cheaper. The binary formats ([`from_cbor`](../basic_json/from_cbor.md),
[`from_msgpack`](../basic_json/from_msgpack.md), ...) read from a `FILE*` the same way.
!!! note "Locking"
While a value is parsed from a `FILE*`, the parsing thread holds the file's lock. The lock is recursive, so the
parsing thread itself - for instance, a [SAX](../json_sax/index.md) handler or a
[parser callback](../basic_json/parser_callback_t.md) - can keep using the file with stdio functions such as
`std::ftell`. Other threads that use the same `FILE` object wait until the parser is done. Before, they could
interleave their reads with the parser's, which made the parsed input unpredictable anyway.
A program in which a SAX handler or parser callback waits for **another** thread that uses the **same** `FILE`
object would deadlock. Define the macro to `1` for such programs.
!!! warning "Define consistently"
The macro selects between two definitions of the library's internal `FILE*` input adapter. It must therefore be
defined identically for **every** translation unit that includes the library; mixing translation units that define
it with ones that do not is an ODR violation (and can leave a file locked). Prefer setting it as a compile
definition on the target rather than with `#!cpp #define` in individual source files.
!!! note "Other inputs"
Reading from a `#!cpp std::istream` is not affected by this macro; see
[`JSON_DISABLE_STREAM_BULK_SCAN`](json_disable_stream_bulk_scan.md) for that input.
## Examples
??? example "Example: parsing from a `FILE*` (default)"
The file is locked once while it is parsed:
```cpp
#include <cstdio>
#include <nlohmann/json.hpp>
using json = nlohmann::json;
int main()
{
std::FILE* f = std::fopen("example.json", "rb");
json j = json::parse(f); // f is locked until parse() returns
std::fclose(f);
}
```
??? example "Example: locking for every character (macro defined to 1)"
With the macro, every character is read with `std::fgetc`, with the same result:
```cpp
#define JSON_DISABLE_UNLOCKED_FILE_IO 1
#include <cstdio>
#include <nlohmann/json.hpp>
using json = nlohmann::json;
int main()
{
std::FILE* f = std::fopen("example.json", "rb");
json j = json::parse(f); // std::fgetc for every character
std::fclose(f);
}
```
## See also
- [**parse**](../basic_json/parse.md) - deserialize from a compatible input
- [**sax_parse**](../basic_json/sax_parse.md) - generate SAX events
- [**JSON_DISABLE_STREAM_BULK_SCAN**](json_disable_stream_bulk_scan.md) - read a `std::istream` one character at a
time
- [Parsing: input sources and performance](../../features/parsing/index.md#input-sources-and-performance)
## Version history
- Added in version 3.13.0.
@@ -72,6 +72,12 @@ The default value is `0` (disabled — existing behavior is preserved).
distinct symbol names. Translation units compiled with and without it can therefore be linked into the same program
without One Definition Rule (ODR) violations, but they cannot exchange instances of library types.
!!! note "Bulk scanning"
The parser scans the characters in the stream buffer in bulk (see
[`JSON_DISABLE_STREAM_BULK_SCAN`](json_disable_stream_bulk_scan.md)), but it never reads ahead. The stream is left
at the same position with and without bulk scanning, in both modes of this macro.
!!! tip "Workaround without the macro"
Separate the values in the stream with whitespace. The character consumed after a number is then the separator,
@@ -12,8 +12,9 @@ non-ASCII characters (e.g. CJK or emoji) this can validate several times faster.
This is an **opt-in external dependency**. The library itself remains header-only and its behavior is unchanged: the
same input is accepted or rejected either way, and every parse error is reported at the same position with the same
message (simdutf is only used to fast-path *valid* runs; anything it flags falls back to the scalar path so the exact
diagnostic is preserved). Streaming inputs (files, `std::istream`, wide strings, user-defined adapters) always use the
scalar path.
diagnostic is preserved). A `std::istream` uses simdutf for the characters its stream buffer holds (see
[`JSON_DISABLE_STREAM_BULK_SCAN`](json_disable_stream_bulk_scan.md)); other streaming inputs (`FILE*`, wide strings,
user-defined adapters) always use the scalar path.
When `JSON_USE_SIMDUTF` is defined you must make the `simdutf.h` header available on the include path and link the
simdutf library. When it is not defined, no simdutf header is included and there is no dependency.
+4
View File
@@ -121,6 +121,7 @@ being read.
- [`JSON_STRICT_NUL_HANDLING`](macros/json_strict_nul_handling.md) - opt in to rejecting a NUL byte in the input
instead of treating it as end of input
- [`JSON_PRECISE_STREAM_POSITION`](macros/json_precise_stream_position.md) - opt in to leaving the stream positioned right after a number
- [`JSON_DISABLE_STREAM_BULK_SCAN`](macros/json_disable_stream_bulk_scan.md) - read the stream one character at a time
## Version history
@@ -133,3 +134,6 @@ being read.
crash (`std::terminate`) when `i` has `eofbit` in its exception mask, in version 3.13.0.
- Changed to the strong exception safety guarantee in version 3.13.0: `j` is no longer left with a partially parsed
value if parsing throws.
- Scans the characters in the stream buffer of `i` in bulk in version 3.13.0; the position of `i` after the call is
unchanged ([`JSON_DISABLE_STREAM_BULK_SCAN`](macros/json_disable_stream_bulk_scan.md) restores reading one character
at a time).
+16
View File
@@ -133,6 +133,14 @@ When defined, default parse and serialize functions for enums are excluded and h
See [full documentation of `JSON_DISABLE_ENUM_SERIALIZATION`](../api/macros/json_disable_enum_serialization.md).
## `JSON_DISABLE_STREAM_BULK_SCAN`
When defined to `1`, parsing from a `#!cpp std::istream` reads one character at a time instead of scanning the
characters that the stream buffer holds in bulk. Results are the same either way; the macro is an escape hatch for
stream buffers that do not follow the usual `std::streambuf` contract. The default value is `0`.
See [full documentation of `JSON_DISABLE_STREAM_BULK_SCAN`](../api/macros/json_disable_stream_bulk_scan.md).
## `JSON_DISABLE_TUPLE_REFERENCE_CONVERSION`
When defined to `1`, a JSON value can no longer be created from a one-element `std::tuple` holding a reference to a JSON
@@ -141,6 +149,14 @@ is planned to become the default in version 4.0.0.
See [full documentation of `JSON_DISABLE_TUPLE_REFERENCE_CONVERSION`](../api/macros/json_disable_tuple_reference_conversion.md).
## `JSON_DISABLE_UNLOCKED_FILE_IO`
When defined to `1`, parsing from a `#!cpp FILE*` reads every character with `std::fgetc`, which locks the file each
time, instead of locking the file once and reading it with the unlocked stdio functions. Results are the same either
way. The default value is `0`.
See [full documentation of `JSON_DISABLE_UNLOCKED_FILE_IO`](../api/macros/json_disable_unlocked_file_io.md).
## `JSON_NO_AUTOMATIC_UDLS`
When defined, `<nlohmann/json.hpp>` does not include `<nlohmann/json_literals.hpp>` with the user-defined string literals
@@ -55,6 +55,30 @@ whitespace, because `operator>>` consumes the character that terminates a number
[`JSON_PRECISE_STREAM_POSITION`](../../api/macros/json_precise_stream_position.md) is defined to `1` — see the
[`operator>>` notes](../../api/operator_gtgt.md#notes) for details and examples.
## Input sources and performance
All inputs produce the same values and the same exceptions, but they are read in different ways, which makes some of
them faster than others:
| input | how it is read |
|--------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------|
| strings, string views, string literals, contiguous containers, pointer or iterator ranges over contiguous memory | the whole input is scanned in bulk: runs of string characters, numbers, and indentation are recognized several bytes at a time |
| `#!cpp std::istream` (`#!cpp std::ifstream`, `#!cpp std::istringstream`, ...) | the characters the stream buffer holds are scanned in bulk like a string; the stream buffer is asked for more when they are used up |
| `#!cpp FILE*` | one character at a time, with the file locked once for the whole parse |
| other iterator ranges (e.g., `#!cpp std::list<char>`, `#!cpp std::istreambuf_iterator<char>`), wide strings | one character at a time |
Parsing a file from a `#!cpp std::ifstream` is therefore only about 10 to 20 % slower than reading the whole file into a
`#!cpp std::string` first and parsing that, and it needs no memory for a copy of the file. The stream is never read
ahead: after parsing, it is positioned exactly as if every character had been read one at a time, so
[`operator>>`](../../api/operator_gtgt.md) can still read several values from one stream (see also
[JSON Lines](json_lines.md)).
Two macros switch these optimizations off without changing any result:
[`JSON_DISABLE_STREAM_BULK_SCAN`](../../api/macros/json_disable_stream_bulk_scan.md) for stream buffers that do not
follow the usual `std::streambuf` contract, and
[`JSON_DISABLE_UNLOCKED_FILE_IO`](../../api/macros/json_disable_unlocked_file_io.md) for programs in which another
thread must use the same `#!cpp FILE` object while it is parsed.
## SAX vs. DOM parsing
The library offers two parsing models:
+2
View File
@@ -297,7 +297,9 @@ nav:
- 'JSON_DELETE_DEPRECATED_FUNCTIONS': api/macros/json_delete_deprecated_functions.md
- 'JSON_DIAGNOSTIC_POSITIONS': api/macros/json_diagnostic_positions.md
- 'JSON_DISABLE_ENUM_SERIALIZATION': api/macros/json_disable_enum_serialization.md
- 'JSON_DISABLE_STREAM_BULK_SCAN': api/macros/json_disable_stream_bulk_scan.md
- 'JSON_DISABLE_TUPLE_REFERENCE_CONVERSION': api/macros/json_disable_tuple_reference_conversion.md
- 'JSON_DISABLE_UNLOCKED_FILE_IO': api/macros/json_disable_unlocked_file_io.md
- 'JSON_HAS_CPP_11, JSON_HAS_CPP_14, JSON_HAS_CPP_17, JSON_HAS_CPP_20, JSON_HAS_CPP_23, JSON_HAS_CPP_26': api/macros/json_has_cpp_11.md
- 'JSON_HAS_EXPERIMENTAL_FILESYSTEM, JSON_HAS_FILESYSTEM': api/macros/json_has_filesystem.md
- 'JSON_HAS_RANGE_VIEW_CONVERSION': api/macros/json_has_range_view_conversion.md
+293 -11
View File
@@ -14,10 +14,12 @@
#include <cstdint> // uint32_t
#include <cstring> // strlen
#include <iterator> // begin, end, iterator_traits, random_access_iterator_tag, distance, next, reverse_iterator
#include <limits> // numeric_limits
#include <streambuf> // streambuf
#include <string> // string, char_traits
#include <type_traits> // enable_if, is_base_of, is_pointer, is_integral, remove_pointer
#include <utility> // pair, declval
#include <vector> // vector
#ifndef JSON_NO_IO
#include <cstdio> // FILE *
@@ -30,6 +32,24 @@
#include <nlohmann/detail/meta/type_traits.hpp>
#include <nlohmann/detail/string_utils.hpp>
#ifndef JSON_NO_IO
// Which unlocked stdio functions file_input_adapter uses (see
// JSON_DISABLE_UNLOCKED_FILE_IO): 1 = POSIX flockfile/getc_unlocked,
// 2 = Microsoft CRT _lock_file/_fgetc_nolock, 0 = none (std::fgetc, which
// locks the file for every character). Only C libraries known to declare
// the functions are listed; the macros are tested after <cstdio>, which
// defines __GLIBC__.
#if JSON_DISABLE_UNLOCKED_FILE_IO
#define JSON_UNLOCKED_FILE_IO 0
#elif defined(_MSC_VER)
#define JSON_UNLOCKED_FILE_IO 2
#elif defined(__GLIBC__) || defined(__APPLE__) || defined(__ANDROID__) || defined(__FreeBSD__) || defined(__NetBSD__) || defined(__OpenBSD__) || defined(__DragonFly__)
#define JSON_UNLOCKED_FILE_IO 1
#else
#define JSON_UNLOCKED_FILE_IO 0
#endif
#endif // JSON_NO_IO
NLOHMANN_JSON_NAMESPACE_BEGIN
namespace detail
{
@@ -56,18 +76,38 @@ class file_input_adapter
: m_file(f)
{
JSON_ASSERT(m_file != nullptr);
lock();
}
// make class move-only
file_input_adapter(const file_input_adapter&) = delete;
file_input_adapter(file_input_adapter&&) noexcept = default;
file_input_adapter(file_input_adapter&& rhs) noexcept
: m_file(rhs.m_file)
{
// the lock is owned by the thread, not the adapter: hand it over
rhs.m_file = nullptr;
}
file_input_adapter& operator=(const file_input_adapter&) = delete;
file_input_adapter& operator=(file_input_adapter&&) = delete;
~file_input_adapter() = default;
~file_input_adapter()
{
if (m_file != nullptr)
{
unlock();
}
}
std::char_traits<char>::int_type get_character() noexcept
{
// the file is locked for the lifetime of the adapter, so the
// per-character lock of std::fgetc can be skipped
#if JSON_UNLOCKED_FILE_IO == 1
return getc_unlocked(m_file);
#elif JSON_UNLOCKED_FILE_IO == 2
return _fgetc_nolock(m_file);
#else
return std::fgetc(m_file);
#endif
}
// returns the number of characters successfully read
@@ -78,6 +118,27 @@ class file_input_adapter
}
private:
// Lock the file once for the whole parse instead of once per character.
// The lock is recursive, so fread() and the user's own stdio calls on the
// same thread keep working.
void lock() noexcept
{
#if JSON_UNLOCKED_FILE_IO == 1
flockfile(m_file);
#elif JSON_UNLOCKED_FILE_IO == 2
_lock_file(m_file);
#endif
}
void unlock() noexcept
{
#if JSON_UNLOCKED_FILE_IO == 1
funlockfile(m_file);
#elif JSON_UNLOCKED_FILE_IO == 2
_unlock_file(m_file);
#endif
}
/// the file pointer to read from
std::FILE* m_file;
};
@@ -127,15 +188,26 @@ class input_stream_adapter
input_stream_adapter& operator=(input_stream_adapter&) = delete;
input_stream_adapter& operator=(input_stream_adapter&&) = delete;
#if JSON_PRECISE_STREAM_POSITION
input_stream_adapter(input_stream_adapter&& rhs) noexcept
: is(rhs.is), sb(rhs.sb), lookahead(rhs.lookahead)
: is(rhs.is), sb(rhs.sb)
#if JSON_PRECISE_STREAM_POSITION
, lookahead(rhs.lookahead)
#endif
#if !JSON_DISABLE_STREAM_BULK_SCAN
, token_carry(std::move(rhs.token_carry))
, token_segment(rhs.token_segment)
, current_in_get_area(rhs.current_in_get_area)
, tracking_token(rhs.tracking_token)
#endif
{
rhs.is = nullptr;
rhs.sb = nullptr;
#if JSON_PRECISE_STREAM_POSITION
rhs.lookahead = false;
#endif
}
#if JSON_PRECISE_STREAM_POSITION
// Whether the character last returned by get_character() can be given back
// to the input with release_lookahead().
static constexpr bool supports_lookahead = true;
@@ -155,17 +227,41 @@ class input_stream_adapter
sb->sbumpc();
}
#if !JSON_DISABLE_STREAM_BULK_SCAN
// sgetc() refills the get area if it is exhausted
const bool refill = streambuf_access::get_ptr(sb) == streambuf_access::get_end(sb);
if (JSON_HEDLEY_UNLIKELY(refill))
{
save_token_segment();
}
#endif
auto res = sb->sgetc();
// set eof manually, as we don't use the istream interface.
if (JSON_HEDLEY_UNLIKELY(res == std::char_traits<char>::eof()))
{
// there is nothing to step over next time
lookahead = false;
#if !JSON_DISABLE_STREAM_BULK_SCAN
current_in_get_area = false;
#endif
is->clear(is->rdstate() | std::ios::eofbit);
}
else
{
lookahead = true;
#if !JSON_DISABLE_STREAM_BULK_SCAN
if (JSON_HEDLEY_UNLIKELY(refill))
{
// the peeked character is at gptr(), unless the streambuf has
// no get area
after_refill(res, streambuf_access::get_ptr(sb) != streambuf_access::get_end(sb));
}
else
{
current_in_get_area = true;
}
#endif
}
return res;
}
@@ -179,13 +275,6 @@ class input_stream_adapter
lookahead = false;
}
#else
input_stream_adapter(input_stream_adapter&& rhs) noexcept
: is(rhs.is), sb(rhs.sb)
{
rhs.is = nullptr;
rhs.sb = nullptr;
}
// std::istream/std::streambuf use std::char_traits<char>::to_int_type, to
// ensure that std::char_traits<char>::eof() and the character 0xFF do not
// end up as the same value, e.g., 0xFFFFFFFF.
@@ -194,12 +283,37 @@ class input_stream_adapter
// stays consumed after parsing; see JSON_PRECISE_STREAM_POSITION.
std::char_traits<char>::int_type get_character()
{
#if !JSON_DISABLE_STREAM_BULK_SCAN
// sbumpc() refills the get area if it is exhausted
const bool refill = streambuf_access::get_ptr(sb) == streambuf_access::get_end(sb);
if (JSON_HEDLEY_UNLIKELY(refill))
{
save_token_segment();
}
#endif
auto res = sb->sbumpc();
// set eof manually, as we don't use the istream interface.
if (JSON_HEDLEY_UNLIKELY(res == std::char_traits<char>::eof()))
{
#if !JSON_DISABLE_STREAM_BULK_SCAN
current_in_get_area = false;
#endif
is->clear(is->rdstate() | std::ios::eofbit);
}
#if !JSON_DISABLE_STREAM_BULK_SCAN
else if (JSON_HEDLEY_UNLIKELY(refill))
{
// the consumed character is at gptr() - 1, unless the streambuf
// has no get area and returned it from uflow()
const char* const next = streambuf_access::get_ptr(sb);
after_refill(res, next != nullptr && next != streambuf_access::get_begin(sb));
}
else
{
current_in_get_area = true;
}
#endif
return res;
}
#endif
@@ -218,7 +332,165 @@ class input_stream_adapter
return res;
}
#if !JSON_DISABLE_STREAM_BULK_SCAN
// The lexer may scan the characters in the get area of the streambuf
// directly (SWAR fast paths). The get area is only a window into the
// input: it ends wherever the streambuf's buffer ends, and is refilled by
// the next get_character(). The lexer therefore keeps counting the
// position itself and treats the end of the window as "unknown", not as
// the end of the input.
static constexpr bool supports_bulk_scan = true;
static constexpr bool bulk_window = true;
// Pointer to the next unread character; only valid when
// bulk_remaining() > 0. The character before it is the one last returned
// by get_character().
const char* bulk_data() const
{
return current_end();
}
// Number of unread characters in the get area.
std::size_t bulk_remaining() const
{
if (!current_in_get_area)
{
return 0;
}
return static_cast<std::size_t>(streambuf_access::get_end(sb) - current_end());
}
// Consume @a n characters previously inspected via bulk_data(); the last
// of them becomes the character last returned by get_character().
void bulk_skip(std::size_t n)
{
JSON_ASSERT(n <= bulk_remaining());
// gbump() takes an int, but a get area (e.g., of a std::stringbuf) can
// be larger
constexpr auto max_bump = static_cast<std::size_t>((std::numeric_limits<int>::max)());
for (; n > max_bump; n -= max_bump)
{
streambuf_access::bump(sb, static_cast<int>(max_bump));
}
streambuf_access::bump(sb, static_cast<int>(n));
}
// The adapter keeps the current token for the lexer's error messages: the
// part in the get area is only remembered as a pointer, and copied out
// when the get area is about to be refilled.
static constexpr bool supports_token_tracking = true;
// Start a token with the characters read from now on. Until this is
// called, nothing is tracked (the binary readers never call it).
void begin_token() noexcept
{
tracking_token = true;
token_carry.clear();
token_segment = current_end();
}
// Start a new token with the character @a c last returned by
// get_character().
void mark_token_start(std::char_traits<char>::int_type c)
{
tracking_token = true;
token_carry.clear();
if (current_in_get_area)
{
token_segment = current_end() - 1;
}
else
{
token_segment = nullptr;
token_carry.push_back(std::char_traits<char>::to_char_type(c));
}
}
// Copy the characters read since mark_token_start() to @a out, without the
// last one if @a drop_last.
void copy_token(std::vector<char>& out, bool drop_last) const
{
out = token_carry;
if (token_segment != nullptr)
{
out.insert(out.end(), token_segment, current_end());
}
if (drop_last && !out.empty())
{
out.pop_back();
}
}
#endif
private:
#if !JSON_DISABLE_STREAM_BULK_SCAN
// Reads the protected get-area pointers of any std::streambuf. Naming the
// members through a derived class makes forming the member pointers
// accessible; calling them on another streambuf object is then ordinary
// member access through a pointer to member.
struct streambuf_access : std::streambuf
{
static const char* get_ptr(std::streambuf* s)
{
return (s->*(&streambuf_access::gptr))();
}
static const char* get_end(std::streambuf* s)
{
return (s->*(&streambuf_access::egptr))();
}
static const char* get_begin(std::streambuf* s)
{
return (s->*(&streambuf_access::eback))();
}
static void bump(std::streambuf* s, int n)
{
(s->*(&streambuf_access::gbump))(n);
}
};
// one past the character last returned by get_character(), if that is in
// the get area
const char* current_end() const
{
#if JSON_PRECISE_STREAM_POSITION
// the character is peeked at gptr() and not stepped over yet, unless
// it was given back with release_lookahead()
return streambuf_access::get_ptr(sb) + (lookahead ? 1 : 0);
#else
return streambuf_access::get_ptr(sb);
#endif
}
// the get area is about to be replaced: copy the token's part in it
void save_token_segment()
{
if (token_segment != nullptr)
{
token_carry.insert(token_carry.end(), token_segment, streambuf_access::get_end(sb));
token_segment = nullptr;
}
}
// character @a c was just read into a new get area (@a in_get_area) or
// returned without one
void after_refill(std::char_traits<char>::int_type c, bool in_get_area)
{
current_in_get_area = in_get_area;
if (!tracking_token)
{
return;
}
if (in_get_area)
{
token_segment = current_end() - 1;
}
else
{
token_carry.push_back(std::char_traits<char>::to_char_type(c));
}
}
#endif
#if JSON_PRECISE_STREAM_POSITION
// Step over the character last returned by get_character(). The character
// has already been peeked successfully, so for every streambuf with a get
@@ -240,6 +512,16 @@ class input_stream_adapter
/// whether get_character() peeked a character that is not consumed yet
bool lookahead = false;
#endif
#if !JSON_DISABLE_STREAM_BULK_SCAN
/// the part of the current token that was in earlier get areas
std::vector<char> token_carry{}; // NOLINT(readability-redundant-member-init)
/// start of the current token's part in the get area, or nullptr
const char* token_segment = nullptr;
/// whether the character last returned by get_character() is in the get area
bool current_in_get_area = false;
/// whether a token is tracked at all; the binary readers never start one
bool tracking_token = false;
#endif
};
#endif // JSON_NO_IO
+165 -18
View File
@@ -166,6 +166,43 @@ constexpr bool input_adapter_supports_bulk_scan(std::false_type /*detected*/)
return false;
}
// Detect whether an input adapter keeps the current token itself and can
// hand it out on demand (see input_stream_adapter::supports_token_tracking),
// so the lexer need not copy every scanned character into token_string.
template<typename InputAdapterType>
using detect_supports_token_tracking = decltype(InputAdapterType::supports_token_tracking);
template<typename InputAdapterType>
constexpr bool input_adapter_supports_token_tracking(std::true_type /*detected*/)
{
return InputAdapterType::supports_token_tracking;
}
template<typename InputAdapterType>
constexpr bool input_adapter_supports_token_tracking(std::false_type /*detected*/)
{
return false;
}
// Detect whether the contiguous block of a bulk-scan adapter is only a window
// into the input (see input_stream_adapter::bulk_window): its end is not the
// end of the input, so a token that touches it must be finished character by
// character.
template<typename InputAdapterType>
using detect_bulk_window = decltype(InputAdapterType::bulk_window);
template<typename InputAdapterType>
constexpr bool input_adapter_bulk_window(std::true_type /*detected*/)
{
return InputAdapterType::bulk_window;
}
template<typename InputAdapterType>
constexpr bool input_adapter_bulk_window(std::false_type /*detected*/)
{
return false;
}
/*!
@brief lexical analysis
@@ -184,9 +221,20 @@ class lexer : public lexer_base<BasicJsonType>
/// whether the last read token can be reconstructed from the input adapter
/// on demand (in error paths) instead of being copied on every scanned
/// character; see input_adapter_supports_seek
static constexpr bool lazy_token_string =
static constexpr bool seekable =
input_adapter_supports_seek<InputAdapterType>(is_detected<detect_supports_seek, InputAdapterType> {});
/// whether the adapter keeps the token itself (mark_token_start() /
/// copy_token()); see input_adapter_supports_token_tracking
static constexpr bool adapter_token =
!seekable && input_adapter_supports_token_tracking<InputAdapterType>(is_detected<detect_supports_token_tracking, InputAdapterType> {});
/// whether the last read token is obtained from the input adapter on
/// demand (in error paths) instead of being copied on every scanned
/// character: seekable adapters reconstruct it from the consumed input,
/// token-tracking adapters keep it themselves
static constexpr bool lazy_token_string = seekable || adapter_token;
/// whether the read position is derived from the input adapter on demand
/// instead of being counted on every scanned character. Seekable adapters
/// know how much input they consumed and can revisit it, so
@@ -194,7 +242,7 @@ class lexer : public lexer_base<BasicJsonType>
/// and line and column are recomputed from the consumed input only when
/// get_position() is called (errors). Counting per character kept two
/// increments and a newline check in the hot path of every get().
static constexpr bool lazy_position = lazy_token_string;
static constexpr bool lazy_position = seekable;
/// whether a simulated unget can be passed on to the input adapter, which
/// then leaves the character in the input; see
@@ -203,14 +251,17 @@ class lexer : public lexer_base<BasicJsonType>
input_adapter_supports_lookahead<InputAdapterType>(is_detected<detect_supports_lookahead, InputAdapterType> {});
/// whether string scanning may bulk-consume runs of ordinary characters
/// directly from a contiguous input buffer (SWAR fast path). This requires
/// the token to be reconstructible lazily (lazy_token_string), so bypassing
/// the per-character capture in get() cannot lose error diagnostics. It
/// also implies lazy_position, so the bulk paths need not advance the
/// position counters.
/// directly from a contiguous input buffer (SWAR fast path). Without
/// lazy_token_string / lazy_position, the bulk paths copy the consumed
/// characters into token_string and advance the position counters
/// themselves (see bulk_consume).
static constexpr bool bulk_scan =
lazy_token_string
&& input_adapter_supports_bulk_scan<InputAdapterType>(is_detected<detect_supports_bulk_scan, InputAdapterType> {});
input_adapter_supports_bulk_scan<InputAdapterType>(is_detected<detect_supports_bulk_scan, InputAdapterType> {});
/// whether the contiguous block is only a window into the input, so that
/// its end does not end a token; see input_adapter_bulk_window
static constexpr bool bulk_window =
input_adapter_bulk_window<InputAdapterType>(is_detected<detect_bulk_window, InputAdapterType> {});
public:
using token_type = typename lexer_base<BasicJsonType>::token_type;
@@ -219,7 +270,11 @@ class lexer : public lexer_base<BasicJsonType>
: ia(std::move(adapter))
, ignore_comments(ignore_comments_)
, discard_number_values(discard_number_values_)
{}
{
// the token string of an error before the first token is everything
// read so far
begin_token(std::integral_constant<bool, adapter_token> {});
}
// deleted because of pointer members
lexer(const lexer&) = delete;
@@ -342,7 +397,36 @@ class lexer : public lexer_base<BasicJsonType>
return;
}
token_buffer.append(reinterpret_cast<const typename string_t::value_type*>(data), pos);
ia.bulk_skip(pos);
bulk_consume(reinterpret_cast<const char*>(data), pos);
}
/// consume @a n characters at @a data (the adapter's bulk_data()) and do
/// the bookkeeping get() would have done for them; the characters are
/// never newlines
void bulk_consume(const char* data, std::size_t n)
{
bulk_track_position(n, std::integral_constant<bool, lazy_position> {});
bulk_track_token(data, n, std::integral_constant<bool, lazy_token_string> {});
ia.bulk_skip(n);
}
/// seekable adapter: the position is derived from the adapter
void bulk_track_position(std::size_t /*n*/, std::true_type /*lazy*/) const noexcept {}
/// streaming adapter: count the characters
void bulk_track_position(std::size_t n, std::false_type /*lazy*/) noexcept
{
position.chars_read_total += n;
position.chars_read_current_line += n;
}
/// the token is obtained from the adapter
void bulk_track_token(const char* /*data*/, std::size_t /*n*/, std::true_type /*lazy*/) const noexcept {}
/// streaming adapter: capture the characters
void bulk_track_token(const char* data, std::size_t n, std::false_type /*lazy*/)
{
token_string.insert(token_string.end(), data, data + n);
}
/// streaming input: no bulk fast path
@@ -1557,6 +1641,10 @@ scan_number_done:
}
}
const std::size_t len = i;
if (number_may_continue(len == avail, std::integral_constant<bool, bulk_window> {}))
{
return token_type::uninitialized;
}
// reset() records where this token starts (for diagnostics), so it has
// to run before the input position advances below
@@ -1574,7 +1662,8 @@ scan_number_done:
const token_type integer_result = convert_integer(number_type, data, data + len);
if (JSON_HEDLEY_LIKELY(integer_result != token_type::uninitialized))
{
ia.bulk_skip(len - 1);
bulk_consume(data + 1, len - 1);
read_number_terminator(std::integral_constant<bool, bulk_window> {});
return integer_result;
}
// The value does not fit an integer, so this token converts as a
@@ -1590,11 +1679,37 @@ scan_number_done:
token_buffer.append(reinterpret_cast<const typename string_t::value_type*>(data), len);
decimal_point_position = dot_index;
ia.bulk_skip(len - 1);
bulk_consume(data + 1, len - 1);
read_number_terminator(std::integral_constant<bool, bulk_window> {});
return convert_number(number_type, mantissa_end);
}
/// window adapter: a number that reaches the end of the window may continue
/// after it, so scan_number() has to read it character by character
static constexpr bool number_may_continue(bool at_end, std::true_type /*window*/) noexcept
{
return at_end;
}
/// the end of the block is the end of the input, which ends the number
static constexpr bool number_may_continue(bool /*at_end*/, std::false_type /*window*/) noexcept
{
return false;
}
/// window adapter: read the character after a bulk-scanned number and unget
/// it, as scan_number() does. The adapter is a stream that is handed back to
/// the user, who must find the same input consumed either way.
void read_number_terminator(std::true_type /*window*/)
{
get();
unget();
}
/// the input is not handed back, so the terminator can stay unread
void read_number_terminator(std::false_type /*window*/) const noexcept {}
/// contiguous input: try the number fast path, else the byte-path scanner
token_type scan_number_dispatch(std::true_type /*bulk*/)
{
@@ -1650,11 +1765,30 @@ scan_number_done:
/// seekable adapter: remember where the current token starts so it can be
/// reconstructed from the input on error; current has already been
/// consumed, hence the -1
void note_token_start(std::true_type /*lazy*/) noexcept
void note_token_start(std::true_type /*lazy*/)
{
note_token_start_lazy(std::integral_constant<bool, adapter_token> {});
}
void note_token_start_lazy(std::false_type /*adapter_token*/) noexcept
{
token_string_start = ia.get_consumed_count() - 1;
}
/// token-tracking adapter: start tracking the token at the first character
void begin_token(std::true_type /*adapter_token*/) noexcept
{
ia.begin_token();
}
void begin_token(std::false_type /*adapter_token*/) const noexcept {}
/// token-tracking adapter: the token starts with the current character
void note_token_start_lazy(std::true_type /*adapter_token*/)
{
ia.mark_token_start(current);
}
/// streaming adapter: start copying the token eagerly, beginning with the
/// already-read first character
void note_token_start(std::false_type /*lazy*/) noexcept
@@ -1975,6 +2109,20 @@ scan_number_done:
public:
/// seekable adapter: rebuild the last read token from the input on demand
const std::vector<char_type>& collect_token_chars(std::vector<char_type>& out, std::true_type /*lazy*/) const
{
return collect_token_chars_lazy(out, std::integral_constant<bool, adapter_token> {});
}
/// token-tracking adapter: the adapter hands out everything read since
/// mark_token_start(), except a pending unget (see below)
const std::vector<char_type>& collect_token_chars_lazy(std::vector<char_type>& out, std::true_type /*adapter_token*/) const
{
const bool pending_real_unget = next_unget && current != char_traits<char_type>::eof();
ia.copy_token(out, pending_real_unget);
return out;
}
const std::vector<char_type>& collect_token_chars_lazy(std::vector<char_type>& out, std::false_type /*adapter_token*/) const
{
// a pending unget of a real (non-EOF) character means that character
// was consumed from the input but is not part of the token; EOF is
@@ -2096,9 +2244,8 @@ scan_number_done:
Pretty-printed JSON indents every line with a run of spaces, which the loop
in skip_whitespace() would otherwise read one character at a time. The
spaces are skipped 8 bytes at a time; the last 7 bytes of the input and any
other whitespace are left to that loop. Skipping bypasses get(), which is
fine because bulk_scan implies lazy_position: the position is derived from
the adapter, not counted per character.
other whitespace are left to that loop. Skipping bypasses get();
bulk_consume() does its bookkeeping.
*/
void skip_indentation(std::true_type /*bulk*/)
{
@@ -2121,7 +2268,7 @@ scan_number_done:
break;
}
}
ia.bulk_skip(pos);
bulk_consume(data, pos);
}
/// streaming input: no bulk fast path
+8
View File
@@ -895,3 +895,11 @@
#ifndef JSON_DISABLE_TUPLE_REFERENCE_CONVERSION
#define JSON_DISABLE_TUPLE_REFERENCE_CONVERSION 0
#endif
#ifndef JSON_DISABLE_STREAM_BULK_SCAN
#define JSON_DISABLE_STREAM_BULK_SCAN 0
#endif
#ifndef JSON_DISABLE_UNLOCKED_FILE_IO
#define JSON_DISABLE_UNLOCKED_FILE_IO 0
#endif
@@ -21,6 +21,9 @@
#undef JSON_NO_UNIQUE_ADDRESS
#undef JSON_DISABLE_ENUM_SERIALIZATION
#undef JSON_DISABLE_TUPLE_REFERENCE_CONVERSION
#undef JSON_DISABLE_STREAM_BULK_SCAN
#undef JSON_DISABLE_UNLOCKED_FILE_IO
#undef JSON_UNLOCKED_FILE_IO
#ifndef JSON_TEST_KEEP_MACROS
#undef JSON_CATCH
+469 -29
View File
@@ -3238,6 +3238,14 @@ JSON_HEDLEY_DIAGNOSTIC_POP
#define JSON_DISABLE_TUPLE_REFERENCE_CONVERSION 0
#endif
#ifndef JSON_DISABLE_STREAM_BULK_SCAN
#define JSON_DISABLE_STREAM_BULK_SCAN 0
#endif
#ifndef JSON_DISABLE_UNLOCKED_FILE_IO
#define JSON_DISABLE_UNLOCKED_FILE_IO 0
#endif
#if JSON_HAS_THREE_WAY_COMPARISON
#include <compare> // partial_ordering
#endif
@@ -7974,10 +7982,12 @@ NLOHMANN_JSON_NAMESPACE_END
#include <cstdint> // uint32_t
#include <cstring> // strlen
#include <iterator> // begin, end, iterator_traits, random_access_iterator_tag, distance, next, reverse_iterator
#include <limits> // numeric_limits
#include <streambuf> // streambuf
#include <string> // string, char_traits
#include <type_traits> // enable_if, is_base_of, is_pointer, is_integral, remove_pointer
#include <utility> // pair, declval
#include <vector> // vector
#ifndef JSON_NO_IO
#include <cstdio> // FILE *
@@ -7995,6 +8005,24 @@ NLOHMANN_JSON_NAMESPACE_END
// #include <nlohmann/detail/string_utils.hpp>
#ifndef JSON_NO_IO
// Which unlocked stdio functions file_input_adapter uses (see
// JSON_DISABLE_UNLOCKED_FILE_IO): 1 = POSIX flockfile/getc_unlocked,
// 2 = Microsoft CRT _lock_file/_fgetc_nolock, 0 = none (std::fgetc, which
// locks the file for every character). Only C libraries known to declare
// the functions are listed; the macros are tested after <cstdio>, which
// defines __GLIBC__.
#if JSON_DISABLE_UNLOCKED_FILE_IO
#define JSON_UNLOCKED_FILE_IO 0
#elif defined(_MSC_VER)
#define JSON_UNLOCKED_FILE_IO 2
#elif defined(__GLIBC__) || defined(__APPLE__) || defined(__ANDROID__) || defined(__FreeBSD__) || defined(__NetBSD__) || defined(__OpenBSD__) || defined(__DragonFly__)
#define JSON_UNLOCKED_FILE_IO 1
#else
#define JSON_UNLOCKED_FILE_IO 0
#endif
#endif // JSON_NO_IO
NLOHMANN_JSON_NAMESPACE_BEGIN
namespace detail
{
@@ -8021,18 +8049,38 @@ class file_input_adapter
: m_file(f)
{
JSON_ASSERT(m_file != nullptr);
lock();
}
// make class move-only
file_input_adapter(const file_input_adapter&) = delete;
file_input_adapter(file_input_adapter&&) noexcept = default;
file_input_adapter(file_input_adapter&& rhs) noexcept
: m_file(rhs.m_file)
{
// the lock is owned by the thread, not the adapter: hand it over
rhs.m_file = nullptr;
}
file_input_adapter& operator=(const file_input_adapter&) = delete;
file_input_adapter& operator=(file_input_adapter&&) = delete;
~file_input_adapter() = default;
~file_input_adapter()
{
if (m_file != nullptr)
{
unlock();
}
}
std::char_traits<char>::int_type get_character() noexcept
{
// the file is locked for the lifetime of the adapter, so the
// per-character lock of std::fgetc can be skipped
#if JSON_UNLOCKED_FILE_IO == 1
return getc_unlocked(m_file);
#elif JSON_UNLOCKED_FILE_IO == 2
return _fgetc_nolock(m_file);
#else
return std::fgetc(m_file);
#endif
}
// returns the number of characters successfully read
@@ -8043,6 +8091,27 @@ class file_input_adapter
}
private:
// Lock the file once for the whole parse instead of once per character.
// The lock is recursive, so fread() and the user's own stdio calls on the
// same thread keep working.
void lock() noexcept
{
#if JSON_UNLOCKED_FILE_IO == 1
flockfile(m_file);
#elif JSON_UNLOCKED_FILE_IO == 2
_lock_file(m_file);
#endif
}
void unlock() noexcept
{
#if JSON_UNLOCKED_FILE_IO == 1
funlockfile(m_file);
#elif JSON_UNLOCKED_FILE_IO == 2
_unlock_file(m_file);
#endif
}
/// the file pointer to read from
std::FILE* m_file;
};
@@ -8092,15 +8161,26 @@ class input_stream_adapter
input_stream_adapter& operator=(input_stream_adapter&) = delete;
input_stream_adapter& operator=(input_stream_adapter&&) = delete;
#if JSON_PRECISE_STREAM_POSITION
input_stream_adapter(input_stream_adapter&& rhs) noexcept
: is(rhs.is), sb(rhs.sb), lookahead(rhs.lookahead)
: is(rhs.is), sb(rhs.sb)
#if JSON_PRECISE_STREAM_POSITION
, lookahead(rhs.lookahead)
#endif
#if !JSON_DISABLE_STREAM_BULK_SCAN
, token_carry(std::move(rhs.token_carry))
, token_segment(rhs.token_segment)
, current_in_get_area(rhs.current_in_get_area)
, tracking_token(rhs.tracking_token)
#endif
{
rhs.is = nullptr;
rhs.sb = nullptr;
#if JSON_PRECISE_STREAM_POSITION
rhs.lookahead = false;
#endif
}
#if JSON_PRECISE_STREAM_POSITION
// Whether the character last returned by get_character() can be given back
// to the input with release_lookahead().
static constexpr bool supports_lookahead = true;
@@ -8120,17 +8200,41 @@ class input_stream_adapter
sb->sbumpc();
}
#if !JSON_DISABLE_STREAM_BULK_SCAN
// sgetc() refills the get area if it is exhausted
const bool refill = streambuf_access::get_ptr(sb) == streambuf_access::get_end(sb);
if (JSON_HEDLEY_UNLIKELY(refill))
{
save_token_segment();
}
#endif
auto res = sb->sgetc();
// set eof manually, as we don't use the istream interface.
if (JSON_HEDLEY_UNLIKELY(res == std::char_traits<char>::eof()))
{
// there is nothing to step over next time
lookahead = false;
#if !JSON_DISABLE_STREAM_BULK_SCAN
current_in_get_area = false;
#endif
is->clear(is->rdstate() | std::ios::eofbit);
}
else
{
lookahead = true;
#if !JSON_DISABLE_STREAM_BULK_SCAN
if (JSON_HEDLEY_UNLIKELY(refill))
{
// the peeked character is at gptr(), unless the streambuf has
// no get area
after_refill(res, streambuf_access::get_ptr(sb) != streambuf_access::get_end(sb));
}
else
{
current_in_get_area = true;
}
#endif
}
return res;
}
@@ -8144,13 +8248,6 @@ class input_stream_adapter
lookahead = false;
}
#else
input_stream_adapter(input_stream_adapter&& rhs) noexcept
: is(rhs.is), sb(rhs.sb)
{
rhs.is = nullptr;
rhs.sb = nullptr;
}
// std::istream/std::streambuf use std::char_traits<char>::to_int_type, to
// ensure that std::char_traits<char>::eof() and the character 0xFF do not
// end up as the same value, e.g., 0xFFFFFFFF.
@@ -8159,12 +8256,37 @@ class input_stream_adapter
// stays consumed after parsing; see JSON_PRECISE_STREAM_POSITION.
std::char_traits<char>::int_type get_character()
{
#if !JSON_DISABLE_STREAM_BULK_SCAN
// sbumpc() refills the get area if it is exhausted
const bool refill = streambuf_access::get_ptr(sb) == streambuf_access::get_end(sb);
if (JSON_HEDLEY_UNLIKELY(refill))
{
save_token_segment();
}
#endif
auto res = sb->sbumpc();
// set eof manually, as we don't use the istream interface.
if (JSON_HEDLEY_UNLIKELY(res == std::char_traits<char>::eof()))
{
#if !JSON_DISABLE_STREAM_BULK_SCAN
current_in_get_area = false;
#endif
is->clear(is->rdstate() | std::ios::eofbit);
}
#if !JSON_DISABLE_STREAM_BULK_SCAN
else if (JSON_HEDLEY_UNLIKELY(refill))
{
// the consumed character is at gptr() - 1, unless the streambuf
// has no get area and returned it from uflow()
const char* const next = streambuf_access::get_ptr(sb);
after_refill(res, next != nullptr && next != streambuf_access::get_begin(sb));
}
else
{
current_in_get_area = true;
}
#endif
return res;
}
#endif
@@ -8183,7 +8305,165 @@ class input_stream_adapter
return res;
}
#if !JSON_DISABLE_STREAM_BULK_SCAN
// The lexer may scan the characters in the get area of the streambuf
// directly (SWAR fast paths). The get area is only a window into the
// input: it ends wherever the streambuf's buffer ends, and is refilled by
// the next get_character(). The lexer therefore keeps counting the
// position itself and treats the end of the window as "unknown", not as
// the end of the input.
static constexpr bool supports_bulk_scan = true;
static constexpr bool bulk_window = true;
// Pointer to the next unread character; only valid when
// bulk_remaining() > 0. The character before it is the one last returned
// by get_character().
const char* bulk_data() const
{
return current_end();
}
// Number of unread characters in the get area.
std::size_t bulk_remaining() const
{
if (!current_in_get_area)
{
return 0;
}
return static_cast<std::size_t>(streambuf_access::get_end(sb) - current_end());
}
// Consume @a n characters previously inspected via bulk_data(); the last
// of them becomes the character last returned by get_character().
void bulk_skip(std::size_t n)
{
JSON_ASSERT(n <= bulk_remaining());
// gbump() takes an int, but a get area (e.g., of a std::stringbuf) can
// be larger
constexpr auto max_bump = static_cast<std::size_t>((std::numeric_limits<int>::max)());
for (; n > max_bump; n -= max_bump)
{
streambuf_access::bump(sb, static_cast<int>(max_bump));
}
streambuf_access::bump(sb, static_cast<int>(n));
}
// The adapter keeps the current token for the lexer's error messages: the
// part in the get area is only remembered as a pointer, and copied out
// when the get area is about to be refilled.
static constexpr bool supports_token_tracking = true;
// Start a token with the characters read from now on. Until this is
// called, nothing is tracked (the binary readers never call it).
void begin_token() noexcept
{
tracking_token = true;
token_carry.clear();
token_segment = current_end();
}
// Start a new token with the character @a c last returned by
// get_character().
void mark_token_start(std::char_traits<char>::int_type c)
{
tracking_token = true;
token_carry.clear();
if (current_in_get_area)
{
token_segment = current_end() - 1;
}
else
{
token_segment = nullptr;
token_carry.push_back(std::char_traits<char>::to_char_type(c));
}
}
// Copy the characters read since mark_token_start() to @a out, without the
// last one if @a drop_last.
void copy_token(std::vector<char>& out, bool drop_last) const
{
out = token_carry;
if (token_segment != nullptr)
{
out.insert(out.end(), token_segment, current_end());
}
if (drop_last && !out.empty())
{
out.pop_back();
}
}
#endif
private:
#if !JSON_DISABLE_STREAM_BULK_SCAN
// Reads the protected get-area pointers of any std::streambuf. Naming the
// members through a derived class makes forming the member pointers
// accessible; calling them on another streambuf object is then ordinary
// member access through a pointer to member.
struct streambuf_access : std::streambuf
{
static const char* get_ptr(std::streambuf* s)
{
return (s->*(&streambuf_access::gptr))();
}
static const char* get_end(std::streambuf* s)
{
return (s->*(&streambuf_access::egptr))();
}
static const char* get_begin(std::streambuf* s)
{
return (s->*(&streambuf_access::eback))();
}
static void bump(std::streambuf* s, int n)
{
(s->*(&streambuf_access::gbump))(n);
}
};
// one past the character last returned by get_character(), if that is in
// the get area
const char* current_end() const
{
#if JSON_PRECISE_STREAM_POSITION
// the character is peeked at gptr() and not stepped over yet, unless
// it was given back with release_lookahead()
return streambuf_access::get_ptr(sb) + (lookahead ? 1 : 0);
#else
return streambuf_access::get_ptr(sb);
#endif
}
// the get area is about to be replaced: copy the token's part in it
void save_token_segment()
{
if (token_segment != nullptr)
{
token_carry.insert(token_carry.end(), token_segment, streambuf_access::get_end(sb));
token_segment = nullptr;
}
}
// character @a c was just read into a new get area (@a in_get_area) or
// returned without one
void after_refill(std::char_traits<char>::int_type c, bool in_get_area)
{
current_in_get_area = in_get_area;
if (!tracking_token)
{
return;
}
if (in_get_area)
{
token_segment = current_end() - 1;
}
else
{
token_carry.push_back(std::char_traits<char>::to_char_type(c));
}
}
#endif
#if JSON_PRECISE_STREAM_POSITION
// Step over the character last returned by get_character(). The character
// has already been peeked successfully, so for every streambuf with a get
@@ -8205,6 +8485,16 @@ class input_stream_adapter
/// whether get_character() peeked a character that is not consumed yet
bool lookahead = false;
#endif
#if !JSON_DISABLE_STREAM_BULK_SCAN
/// the part of the current token that was in earlier get areas
std::vector<char> token_carry{}; // NOLINT(readability-redundant-member-init)
/// start of the current token's part in the get area, or nullptr
const char* token_segment = nullptr;
/// whether the character last returned by get_character() is in the get area
bool current_in_get_area = false;
/// whether a token is tracked at all; the binary readers never start one
bool tracking_token = false;
#endif
};
#endif // JSON_NO_IO
@@ -10612,6 +10902,43 @@ constexpr bool input_adapter_supports_bulk_scan(std::false_type /*detected*/)
return false;
}
// Detect whether an input adapter keeps the current token itself and can
// hand it out on demand (see input_stream_adapter::supports_token_tracking),
// so the lexer need not copy every scanned character into token_string.
template<typename InputAdapterType>
using detect_supports_token_tracking = decltype(InputAdapterType::supports_token_tracking);
template<typename InputAdapterType>
constexpr bool input_adapter_supports_token_tracking(std::true_type /*detected*/)
{
return InputAdapterType::supports_token_tracking;
}
template<typename InputAdapterType>
constexpr bool input_adapter_supports_token_tracking(std::false_type /*detected*/)
{
return false;
}
// Detect whether the contiguous block of a bulk-scan adapter is only a window
// into the input (see input_stream_adapter::bulk_window): its end is not the
// end of the input, so a token that touches it must be finished character by
// character.
template<typename InputAdapterType>
using detect_bulk_window = decltype(InputAdapterType::bulk_window);
template<typename InputAdapterType>
constexpr bool input_adapter_bulk_window(std::true_type /*detected*/)
{
return InputAdapterType::bulk_window;
}
template<typename InputAdapterType>
constexpr bool input_adapter_bulk_window(std::false_type /*detected*/)
{
return false;
}
/*!
@brief lexical analysis
@@ -10630,9 +10957,20 @@ class lexer : public lexer_base<BasicJsonType>
/// whether the last read token can be reconstructed from the input adapter
/// on demand (in error paths) instead of being copied on every scanned
/// character; see input_adapter_supports_seek
static constexpr bool lazy_token_string =
static constexpr bool seekable =
input_adapter_supports_seek<InputAdapterType>(is_detected<detect_supports_seek, InputAdapterType> {});
/// whether the adapter keeps the token itself (mark_token_start() /
/// copy_token()); see input_adapter_supports_token_tracking
static constexpr bool adapter_token =
!seekable && input_adapter_supports_token_tracking<InputAdapterType>(is_detected<detect_supports_token_tracking, InputAdapterType> {});
/// whether the last read token is obtained from the input adapter on
/// demand (in error paths) instead of being copied on every scanned
/// character: seekable adapters reconstruct it from the consumed input,
/// token-tracking adapters keep it themselves
static constexpr bool lazy_token_string = seekable || adapter_token;
/// whether the read position is derived from the input adapter on demand
/// instead of being counted on every scanned character. Seekable adapters
/// know how much input they consumed and can revisit it, so
@@ -10640,7 +10978,7 @@ class lexer : public lexer_base<BasicJsonType>
/// and line and column are recomputed from the consumed input only when
/// get_position() is called (errors). Counting per character kept two
/// increments and a newline check in the hot path of every get().
static constexpr bool lazy_position = lazy_token_string;
static constexpr bool lazy_position = seekable;
/// whether a simulated unget can be passed on to the input adapter, which
/// then leaves the character in the input; see
@@ -10649,14 +10987,17 @@ class lexer : public lexer_base<BasicJsonType>
input_adapter_supports_lookahead<InputAdapterType>(is_detected<detect_supports_lookahead, InputAdapterType> {});
/// whether string scanning may bulk-consume runs of ordinary characters
/// directly from a contiguous input buffer (SWAR fast path). This requires
/// the token to be reconstructible lazily (lazy_token_string), so bypassing
/// the per-character capture in get() cannot lose error diagnostics. It
/// also implies lazy_position, so the bulk paths need not advance the
/// position counters.
/// directly from a contiguous input buffer (SWAR fast path). Without
/// lazy_token_string / lazy_position, the bulk paths copy the consumed
/// characters into token_string and advance the position counters
/// themselves (see bulk_consume).
static constexpr bool bulk_scan =
lazy_token_string
&& input_adapter_supports_bulk_scan<InputAdapterType>(is_detected<detect_supports_bulk_scan, InputAdapterType> {});
input_adapter_supports_bulk_scan<InputAdapterType>(is_detected<detect_supports_bulk_scan, InputAdapterType> {});
/// whether the contiguous block is only a window into the input, so that
/// its end does not end a token; see input_adapter_bulk_window
static constexpr bool bulk_window =
input_adapter_bulk_window<InputAdapterType>(is_detected<detect_bulk_window, InputAdapterType> {});
public:
using token_type = typename lexer_base<BasicJsonType>::token_type;
@@ -10665,7 +11006,11 @@ class lexer : public lexer_base<BasicJsonType>
: ia(std::move(adapter))
, ignore_comments(ignore_comments_)
, discard_number_values(discard_number_values_)
{}
{
// the token string of an error before the first token is everything
// read so far
begin_token(std::integral_constant<bool, adapter_token> {});
}
// deleted because of pointer members
lexer(const lexer&) = delete;
@@ -10788,7 +11133,36 @@ class lexer : public lexer_base<BasicJsonType>
return;
}
token_buffer.append(reinterpret_cast<const typename string_t::value_type*>(data), pos);
ia.bulk_skip(pos);
bulk_consume(reinterpret_cast<const char*>(data), pos);
}
/// consume @a n characters at @a data (the adapter's bulk_data()) and do
/// the bookkeeping get() would have done for them; the characters are
/// never newlines
void bulk_consume(const char* data, std::size_t n)
{
bulk_track_position(n, std::integral_constant<bool, lazy_position> {});
bulk_track_token(data, n, std::integral_constant<bool, lazy_token_string> {});
ia.bulk_skip(n);
}
/// seekable adapter: the position is derived from the adapter
void bulk_track_position(std::size_t /*n*/, std::true_type /*lazy*/) const noexcept {}
/// streaming adapter: count the characters
void bulk_track_position(std::size_t n, std::false_type /*lazy*/) noexcept
{
position.chars_read_total += n;
position.chars_read_current_line += n;
}
/// the token is obtained from the adapter
void bulk_track_token(const char* /*data*/, std::size_t /*n*/, std::true_type /*lazy*/) const noexcept {}
/// streaming adapter: capture the characters
void bulk_track_token(const char* data, std::size_t n, std::false_type /*lazy*/)
{
token_string.insert(token_string.end(), data, data + n);
}
/// streaming input: no bulk fast path
@@ -12003,6 +12377,10 @@ scan_number_done:
}
}
const std::size_t len = i;
if (number_may_continue(len == avail, std::integral_constant<bool, bulk_window> {}))
{
return token_type::uninitialized;
}
// reset() records where this token starts (for diagnostics), so it has
// to run before the input position advances below
@@ -12020,7 +12398,8 @@ scan_number_done:
const token_type integer_result = convert_integer(number_type, data, data + len);
if (JSON_HEDLEY_LIKELY(integer_result != token_type::uninitialized))
{
ia.bulk_skip(len - 1);
bulk_consume(data + 1, len - 1);
read_number_terminator(std::integral_constant<bool, bulk_window> {});
return integer_result;
}
// The value does not fit an integer, so this token converts as a
@@ -12036,11 +12415,37 @@ scan_number_done:
token_buffer.append(reinterpret_cast<const typename string_t::value_type*>(data), len);
decimal_point_position = dot_index;
ia.bulk_skip(len - 1);
bulk_consume(data + 1, len - 1);
read_number_terminator(std::integral_constant<bool, bulk_window> {});
return convert_number(number_type, mantissa_end);
}
/// window adapter: a number that reaches the end of the window may continue
/// after it, so scan_number() has to read it character by character
static constexpr bool number_may_continue(bool at_end, std::true_type /*window*/) noexcept
{
return at_end;
}
/// the end of the block is the end of the input, which ends the number
static constexpr bool number_may_continue(bool /*at_end*/, std::false_type /*window*/) noexcept
{
return false;
}
/// window adapter: read the character after a bulk-scanned number and unget
/// it, as scan_number() does. The adapter is a stream that is handed back to
/// the user, who must find the same input consumed either way.
void read_number_terminator(std::true_type /*window*/)
{
get();
unget();
}
/// the input is not handed back, so the terminator can stay unread
void read_number_terminator(std::false_type /*window*/) const noexcept {}
/// contiguous input: try the number fast path, else the byte-path scanner
token_type scan_number_dispatch(std::true_type /*bulk*/)
{
@@ -12096,11 +12501,30 @@ scan_number_done:
/// seekable adapter: remember where the current token starts so it can be
/// reconstructed from the input on error; current has already been
/// consumed, hence the -1
void note_token_start(std::true_type /*lazy*/) noexcept
void note_token_start(std::true_type /*lazy*/)
{
note_token_start_lazy(std::integral_constant<bool, adapter_token> {});
}
void note_token_start_lazy(std::false_type /*adapter_token*/) noexcept
{
token_string_start = ia.get_consumed_count() - 1;
}
/// token-tracking adapter: start tracking the token at the first character
void begin_token(std::true_type /*adapter_token*/) noexcept
{
ia.begin_token();
}
void begin_token(std::false_type /*adapter_token*/) const noexcept {}
/// token-tracking adapter: the token starts with the current character
void note_token_start_lazy(std::true_type /*adapter_token*/)
{
ia.mark_token_start(current);
}
/// streaming adapter: start copying the token eagerly, beginning with the
/// already-read first character
void note_token_start(std::false_type /*lazy*/) noexcept
@@ -12421,6 +12845,20 @@ scan_number_done:
public:
/// seekable adapter: rebuild the last read token from the input on demand
const std::vector<char_type>& collect_token_chars(std::vector<char_type>& out, std::true_type /*lazy*/) const
{
return collect_token_chars_lazy(out, std::integral_constant<bool, adapter_token> {});
}
/// token-tracking adapter: the adapter hands out everything read since
/// mark_token_start(), except a pending unget (see below)
const std::vector<char_type>& collect_token_chars_lazy(std::vector<char_type>& out, std::true_type /*adapter_token*/) const
{
const bool pending_real_unget = next_unget && current != char_traits<char_type>::eof();
ia.copy_token(out, pending_real_unget);
return out;
}
const std::vector<char_type>& collect_token_chars_lazy(std::vector<char_type>& out, std::false_type /*adapter_token*/) const
{
// a pending unget of a real (non-EOF) character means that character
// was consumed from the input but is not part of the token; EOF is
@@ -12542,9 +12980,8 @@ scan_number_done:
Pretty-printed JSON indents every line with a run of spaces, which the loop
in skip_whitespace() would otherwise read one character at a time. The
spaces are skipped 8 bytes at a time; the last 7 bytes of the input and any
other whitespace are left to that loop. Skipping bypasses get(), which is
fine because bulk_scan implies lazy_position: the position is derived from
the adapter, not counted per character.
other whitespace are left to that loop. Skipping bypasses get();
bulk_consume() does its bookkeeping.
*/
void skip_indentation(std::true_type /*bulk*/)
{
@@ -12567,7 +13004,7 @@ scan_number_done:
break;
}
}
ia.bulk_skip(pos);
bulk_consume(data, pos);
}
/// streaming input: no bulk fast path
@@ -36100,6 +36537,9 @@ struct formatter<nlohmann::NLOHMANN_BASIC_JSON_TPL, char> // NOLINT(cert-dcl58-c
#undef JSON_NO_UNIQUE_ADDRESS
#undef JSON_DISABLE_ENUM_SERIALIZATION
#undef JSON_DISABLE_TUPLE_REFERENCE_CONVERSION
#undef JSON_DISABLE_STREAM_BULK_SCAN
#undef JSON_DISABLE_UNLOCKED_FILE_IO
#undef JSON_UNLOCKED_FILE_IO
#ifndef JSON_TEST_KEEP_MACROS
#undef JSON_CATCH
+31
View File
@@ -285,6 +285,10 @@ if (JSON_TestSimdutf)
endif()
endif()
# test-file_input checks from a second thread that parsing from a FILE* releases the file's lock
find_package(Threads REQUIRED)
json_test_set_test_options("test-file_input;test-file_input_unlocked_io_disabled" LINK_LIBRARIES Threads::Threads)
# *DO* use json_test_set_test_options() above this line
json_test_should_build_32bit_test(json_32bit_test json_32bit_test_only "${JSON_32bitTest}")
@@ -369,6 +373,33 @@ json_test_add_test_for(src/unit-diagnostic-positions.cpp
MAIN test_main CXX_STANDARDS ${test_cxx_standards} ${test_force}
)
# test reading from a std::istream again without scanning the get area in bulk
json_test_set_test_options(test-stream-bulk-scan_disabled
COMPILE_DEFINITIONS JSON_DISABLE_STREAM_BULK_SCAN=1
)
json_test_add_test_for(src/unit-stream-bulk-scan.cpp
NAME test-stream-bulk-scan_disabled
MAIN test_main CXX_STANDARDS ${test_cxx_standards} ${test_force}
)
# test reading from a std::istream again with JSON_PRECISE_STREAM_POSITION
json_test_set_test_options(test-stream-bulk-scan_precise
COMPILE_DEFINITIONS JSON_PRECISE_STREAM_POSITION=1
)
json_test_add_test_for(src/unit-stream-bulk-scan.cpp
NAME test-stream-bulk-scan_precise
MAIN test_main CXX_STANDARDS ${test_cxx_standards} ${test_force}
)
# test reading from a FILE* again with the locking stdio functions
json_test_set_test_options(test-file_input_unlocked_io_disabled
COMPILE_DEFINITIONS JSON_DISABLE_UNLOCKED_FILE_IO=1
)
json_test_add_test_for(src/unit-file_input.cpp
NAME test-file_input_unlocked_io_disabled
MAIN test_main CXX_STANDARDS ${test_cxx_standards} ${test_force}
)
endif()
# *DO NOT* use json_test_set_test_options() below this line
+61
View File
@@ -8,7 +8,9 @@
#include <benchmark/benchmark.h>
#include <nlohmann/json.hpp>
#include <cstdio>
#include <fstream>
#include <sstream>
#include <numeric>
#include <vector>
#include <test_data.hpp>
@@ -48,6 +50,65 @@ BENCHMARK_CAPTURE(ParseFile, signed_ints, TEST_DATA_DIRECTORY "/regression
BENCHMARK_CAPTURE(ParseFile, unsigned_ints, TEST_DATA_DIRECTORY "/regression/unsigned_ints.json");
BENCHMARK_CAPTURE(ParseFile, small_signed_ints, TEST_DATA_DIRECTORY "/regression/small_signed_ints.json");
//////////////////////////////////////////////////////////////////////////////
// parse JSON from a FILE*
//////////////////////////////////////////////////////////////////////////////
static void ParseFILE(benchmark::State& state, const char* filename)
{
while (state.KeepRunning())
{
state.PauseTiming();
std::FILE* f = std::fopen(filename, "rb");
auto* j = new json();
state.ResumeTiming();
*j = json::parse(f);
state.PauseTiming();
std::fclose(f);
delete j;
state.ResumeTiming();
}
std::ifstream file(filename, std::ios::binary | std::ios::ate);
state.SetBytesProcessed(state.iterations() * file.tellg());
}
BENCHMARK_CAPTURE(ParseFILE, canada, TEST_DATA_DIRECTORY "/nativejson-benchmark/canada.json");
BENCHMARK_CAPTURE(ParseFILE, citm_catalog, TEST_DATA_DIRECTORY "/nativejson-benchmark/citm_catalog.json");
BENCHMARK_CAPTURE(ParseFILE, twitter, TEST_DATA_DIRECTORY "/nativejson-benchmark/twitter.json");
//////////////////////////////////////////////////////////////////////////////
// read many small values from one stream (NDJSON)
//////////////////////////////////////////////////////////////////////////////
static void ParseStreamValues(benchmark::State& state, bool numbers)
{
std::string input;
for (int i = 0; i < 10000; ++i)
{
input += numbers ? std::to_string(i * 7919) + " "
: R"({"id":)" + std::to_string(i) + R"(,"name":"user)" + std::to_string(i) + R"(","active":true,"score":)" + std::to_string(i * 0.25) + "}\n";
}
for (auto _ : state)
{
std::istringstream is(input);
json j;
std::size_t count = 0;
while (!(is >> std::ws).eof())
{
is >> j;
++count;
}
benchmark::DoNotOptimize(count);
}
state.SetBytesProcessed(static_cast<int64_t>(state.iterations() * input.size()));
}
BENCHMARK_CAPTURE(ParseStreamValues, ndjson, false);
BENCHMARK_CAPTURE(ParseStreamValues, numbers, true);
//////////////////////////////////////////////////////////////////////////////
// parse JSON from string
//////////////////////////////////////////////////////////////////////////////
+236
View File
@@ -0,0 +1,236 @@
// __ _____ _____ _____
// __| | __| | | | JSON for Modern C++ (supporting code)
// | | |__ | | | | | | version 3.12.0
// |_____|_____|_____|_|___| https://github.com/nlohmann/json
//
// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann <https://nlohmann.me>
// SPDX-License-Identifier: MIT
#include "doctest_compatibility.h"
#include <nlohmann/json.hpp>
using nlohmann::json;
#include <array>
#include <cstdint>
#include <cstdio>
#include <fstream>
#include <memory>
#include <random>
#include <string>
#include <thread>
#include <vector>
// Parsing from a FILE* locks the file once and reads it with the unlocked
// stdio functions (see JSON_DISABLE_UNLOCKED_FILE_IO). These tests pin that
// this is not observable: the results are the same, the file is unlocked
// again when parsing ends - also with an exception - and the same thread can
// keep using the file while it is locked, e.g., from a SAX callback.
namespace
{
// a file name of its own for every run: the test runs for several C++
// standards and configurations at the same time, in the same directory
std::string temp_file_name()
{
std::random_device rd;
return "unit-file_input-" + std::to_string(rd()) + ".tmp";
}
using file_ptr = std::unique_ptr<std::FILE, int (*)(std::FILE*)>;
// write @a content to the file @a name and open it for reading
file_ptr open_with(const char* name, const std::string& content)
{
{
std::ofstream file(name, std::ios::binary);
file << content;
}
return {std::fopen(name, "rb"), &std::fclose};
}
// The stdio lock is recursive, so whether parsing released it can only be
// seen from another thread: ftrylockfile() fails there while this thread
// holds the lock. ftrylockfile() is POSIX; elsewhere, the check is skipped.
#if defined(__GLIBC__) || defined(__APPLE__) || defined(__FreeBSD__) || defined(__NetBSD__) || defined(__OpenBSD__)
void check_unlocked(std::FILE* f)
{
int result = -1;
std::thread other([f, &result]
{
result = ftrylockfile(f);
if (result == 0)
{
funlockfile(f);
}
});
other.join();
CHECK(result == 0);
}
#else
void check_unlocked(std::FILE* /*f*/) {}
#endif
// SAX consumer that also reads the position of the file it is parsing, which
// takes the file's lock again on the same thread
class position_reading_sax : public nlohmann::json_sax<json>
{
public:
explicit position_reading_sax(std::FILE* f) : m_file(f) {}
position_reading_sax(const position_reading_sax&) = delete;
position_reading_sax(position_reading_sax&&) = delete;
position_reading_sax& operator=(const position_reading_sax&) = delete;
position_reading_sax& operator=(position_reading_sax&&) = delete;
~position_reading_sax() override = default;
bool null() override
{
return record();
}
bool boolean(bool /*val*/) override
{
return record();
}
bool number_integer(number_integer_t /*val*/) override
{
return record();
}
bool number_unsigned(number_unsigned_t /*val*/) override
{
return record();
}
bool number_float(number_float_t /*val*/, const string_t& /*s*/) override
{
return record();
}
bool string(string_t& /*val*/) override
{
return record();
}
bool binary(binary_t& /*val*/) override
{
return record();
}
bool start_object(std::size_t /*elements*/) override
{
return record();
}
bool key(string_t& /*val*/) override
{
return record();
}
bool end_object() override
{
return record();
}
bool start_array(std::size_t /*elements*/) override
{
return record();
}
bool end_array() override
{
return record();
}
bool parse_error(std::size_t /*position*/, const std::string& /*last_token*/, const nlohmann::detail::exception& /*ex*/) override
{
return false;
}
const std::vector<long>& positions() const
{
return m_positions;
}
private:
bool record()
{
m_positions.push_back(std::ftell(m_file));
return true;
}
std::FILE* m_file;
std::vector<long> m_positions{}; // NOLINT(readability-redundant-member-init)
};
} // namespace
TEST_CASE("parsing from a FILE*")
{
const std::string temp_name = temp_file_name();
const char* const name = temp_name.c_str();
const std::string input = "{\"a\": [1, -2.5, \"three\", true, null], \"b\": {\"c\": \"caf\xc3\xa9\"}}";
SECTION("values are the same as when parsing from a string")
{
const file_ptr f = open_with(name, input);
REQUIRE(f != nullptr);
CHECK(json::parse(f.get()) == json::parse(input));
// the whole input was consumed
std::array<char, 8> rest{};
CHECK(std::fgets(rest.data(), static_cast<int>(rest.size()), f.get()) == nullptr);
check_unlocked(f.get());
}
SECTION("the file is unlocked after a parse error")
{
const file_ptr f = open_with(name, "[1, 2, x]");
REQUIRE(f != nullptr);
json _;
CHECK_THROWS_WITH_AS(_ = json::parse(f.get()),
"[json.exception.parse_error.101] parse error at line 1, column 8: syntax error while parsing value - invalid literal; last read: '2, x'",
json::parse_error&);
check_unlocked(f.get());
// the file is still usable: the rest of the input is there
std::array<char, 8> rest{};
CHECK(std::fgets(rest.data(), static_cast<int>(rest.size()), f.get()) != nullptr);
CHECK(std::string(rest.data()) == "]");
}
SECTION("the file can be parsed again and read with stdio afterwards")
{
const file_ptr f = open_with(name, "[1] [2] tail");
REQUIRE(f != nullptr);
// sax_parse with strict == false stops after the first value; an array
// ends with its closing bracket, so the space after it is not read
json j;
nlohmann::detail::json_sax_dom_parser<json, nlohmann::detail::string_input_adapter_type> sax1(j);
CHECK(json::sax_parse(f.get(), &sax1, json::input_format_t::json, false));
CHECK(j == json({1}));
check_unlocked(f.get());
json k;
nlohmann::detail::json_sax_dom_parser<json, nlohmann::detail::string_input_adapter_type> sax2(k);
CHECK(json::sax_parse(f.get(), &sax2, json::input_format_t::json, false));
CHECK(k == json({2}));
check_unlocked(f.get());
std::array<char, 8> rest{};
CHECK(std::fgets(rest.data(), static_cast<int>(rest.size()), f.get()) != nullptr);
CHECK(std::string(rest.data()) == " tail");
}
SECTION("the same thread can use the file while it is parsed")
{
const file_ptr f = open_with(name, "[10, 20, 30]");
REQUIRE(f != nullptr);
position_reading_sax sax(f.get());
CHECK(json::sax_parse(f.get(), &sax));
// start_array, three numbers, end_array
CHECK(sax.positions().size() == 5);
CHECK(sax.positions().front() == 1);
check_unlocked(f.get());
}
SECTION("binary formats read from a FILE*")
{
const json j = {{"string", std::string(300, 'x')}, {"array", {1, 2, 3}}, {"number", 1.5}};
const std::vector<std::uint8_t> cbor = json::to_cbor(j);
const file_ptr f = open_with(name, std::string(cbor.begin(), cbor.end()));
REQUIRE(f != nullptr);
CHECK(json::from_cbor(f.get()) == j);
check_unlocked(f.get());
}
static_cast<void>(std::remove(name));
}
@@ -20,6 +20,7 @@
#include <nlohmann/json.hpp>
using nlohmann::json;
#include <algorithm>
#include <cstddef>
#include <sstream>
#include <streambuf>
@@ -72,6 +73,40 @@ class no_putback_streambuf : public std::streambuf
std::size_t m_pos = 0;
};
// A streambuf that serves its input in get areas of a fixed size, so that the
// end of a number falls on every possible offset of a get area. The lexer scans
// the get area directly; the stream position must not depend on its size.
class chunked_streambuf : public std::streambuf
{
public:
chunked_streambuf(std::string s, std::size_t chunk_size)
: m_data(std::move(s)), m_chunk_size(chunk_size)
{}
protected:
int_type underflow() override
{
if (gptr() < egptr())
{
return traits_type::to_int_type(*gptr());
}
m_pos += static_cast<std::size_t>(egptr() - eback());
if (m_pos >= m_data.size())
{
setg(nullptr, nullptr, nullptr);
return traits_type::eof();
}
char* const begin = &m_data[m_pos];
setg(begin, begin, begin + (std::min)(m_chunk_size, m_data.size() - m_pos));
return traits_type::to_int_type(*gptr());
}
private:
std::string m_data;
std::size_t m_pos = 0;
std::size_t m_chunk_size;
};
// read the characters that are left in a stream
std::string remaining(std::istream& is)
{
@@ -263,4 +298,26 @@ TEST_CASE("JSON_PRECISE_STREAM_POSITION")
json _;
CHECK_THROWS_WITH_AS(_ = json::parse(is), "[json.exception.parse_error.101] parse error: attempting to parse an empty input; check that your input string or stream contains the expected JSON", json::parse_error&);
}
SECTION("the position does not depend on the get area of the streambuf")
{
const std::vector<std::size_t> chunk_sizes = {1, 2, 3, 4, 5, 7, 8, 13, 4096};
for (const std::size_t chunk_size : chunk_sizes)
{
CAPTURE(chunk_size)
chunked_streambuf buf("12345678901234567890true\"a long string value\"-1.25e+3,[1,2]42", chunk_size);
std::istream input(&buf);
json j;
input >> j;
CHECK(j == 12345678901234567890u);
input >> j;
CHECK(j == true);
input >> j;
CHECK(j == "a long string value");
input >> j;
CHECK(j == -1250.0);
CHECK(remaining(input) == ",[1,2]42");
}
}
}
+417
View File
@@ -0,0 +1,417 @@
// __ _____ _____ _____
// __| | __| | | | JSON for Modern C++ (supporting code)
// | | |__ | | | | | | version 3.12.0
// |_____|_____|_____|_|___| https://github.com/nlohmann/json
//
// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann <https://nlohmann.me>
// SPDX-License-Identifier: MIT
#include "doctest_compatibility.h"
// the library undefines its configuration macros at the end of the header
#if defined(JSON_DISABLE_STREAM_BULK_SCAN) && JSON_DISABLE_STREAM_BULK_SCAN
#define TEST_STREAM_BULK_SCAN 0
#else
#define TEST_STREAM_BULK_SCAN 1
#endif
#include <nlohmann/json.hpp>
using nlohmann::json;
#include <algorithm>
#include <cstddef>
#include <cstdint>
#include <cstdio>
#include <fstream>
#include <istream>
#include <random>
#include <sstream>
#include <streambuf>
#include <string>
#include <utility>
#include <vector>
// The lexer scans the characters in the get area of a std::istream's
// streambuf directly instead of reading them one at a time. These tests pin
// that this is not observable: for any get area size - including a streambuf
// without a get area, which still takes the character-at-a-time path - the
// parsed values, the exception messages (including the "last read" token),
// the stream state, and the characters left in the stream are the same. See
// unit-stream-bulk-scan-disabled.cpp for JSON_DISABLE_STREAM_BULK_SCAN.
namespace
{
// A streambuf that serves its input in get areas of a fixed size, so that
// tokens, numbers, UTF-8 sequences, and indentation cross the end of the get
// area at every possible offset. With a size of 0, it keeps no get area at all
// and serves every character from underflow()/uflow().
class chunked_streambuf : public std::streambuf
{
public:
chunked_streambuf(std::string s, std::size_t chunk_size)
: m_data(std::move(s)), m_chunk_size(chunk_size)
{}
protected:
int_type underflow() override
{
if (m_chunk_size == 0)
{
return m_pos < m_data.size() ? traits_type::to_int_type(m_data[m_pos]) : traits_type::eof();
}
if (gptr() < egptr())
{
return traits_type::to_int_type(*gptr());
}
// the previous get area has been consumed
m_pos += static_cast<std::size_t>(egptr() - eback());
if (m_pos >= m_data.size())
{
setg(nullptr, nullptr, nullptr);
return traits_type::eof();
}
char* const begin = &m_data[m_pos];
setg(begin, begin, begin + (std::min)(m_chunk_size, m_data.size() - m_pos));
return traits_type::to_int_type(*gptr());
}
int_type uflow() override
{
if (m_chunk_size == 0)
{
return m_pos < m_data.size() ? traits_type::to_int_type(m_data[m_pos++]) : traits_type::eof();
}
return std::streambuf::uflow();
}
private:
std::string m_data;
std::size_t m_pos = 0;
std::size_t m_chunk_size;
};
// how a test reads from the stream
enum class read_mode
{
extract, // up to four times operator>>, as for concatenated values
parse, // json::parse(stream)
parse_lenient, // json::parse(stream) with comments
};
// Read @a input from a chunked_streambuf and describe everything observable:
// the values or the exception, the stream state after each value, and the
// characters left in the stream.
std::string observe(const std::string& input, std::size_t chunk_size, read_mode mode)
{
chunked_streambuf buf(input, chunk_size);
std::istream is(&buf);
std::string result;
for (int i = 0; i < 4; ++i)
{
json j;
try
{
if (mode == read_mode::extract)
{
is >> j;
}
else
{
j = json::parse(is, nullptr, true, mode == read_mode::parse_lenient);
}
result += "value " + j.dump() + "; state " + std::to_string(static_cast<unsigned int>(is.rdstate())) + "\n";
}
catch (const json::exception& e)
{
result += std::string("exception ") + e.what() + "\n";
break;
}
if (mode != read_mode::extract)
{
break;
}
is.clear();
}
is.clear();
result += "remaining ";
char c = 0;
while (is.get(c))
{
result += c;
}
return result;
}
// get area sizes that put every boundary everywhere in short inputs, plus one
// that holds most inputs completely
std::vector<std::size_t> chunk_sizes()
{
return {1, 2, 3, 4, 5, 7, 8, 9, 13, 16, 64, 4096};
}
// valid and invalid inputs covering every token type and every bulk path:
// strings (plain runs, escapes, UTF-8), numbers (all grammar parts, too large
// for an integer), and indentation
std::vector<std::string> corpus()
{
return
{
// numbers, terminated in every way
"0", "-0", "123", "-123", "1.5", "-1.5e+10", "2E-3", "123456789012345678901234567890",
"18446744073709551615", "18446744073709551616", "-9223372036854775809",
"123 456", "123,456", "123]", "1true", "42\nfoo", "1.25e3}", "7\t8\r9",
// malformed numbers
"1.", "-", "1e", "1e+", "01", "-a", "1.x", "1ee5",
// strings
R"("")", R"("abc")", R"("a long string with a run of ordinary characters to scan in bulk")",
R"("escapes \" \\ \/ \b \f \n \r \t é 😀")",
"\"UTF-8: \xc3\xa9 \xe2\x82\xac \xf0\x9f\x98\x80 done\"",
// malformed strings
R"("unterminated)", "\"control\x01character\"", "\"ill-formed \xc3\x28 UTF-8\"", R"("bad \x escape")",
R"("\ud800 lone surrogate")",
// literals
"true", "false", "null", "tru", "nul", "falsey",
// structures
"[]", "{}", "[1,2,3]", R"({"a":1,"b":[true,false,null],"c":{"d":"e"}})", "[1,]", R"({"a" 1})", "[[[[[]]]]]",
// pretty-printed
"{\n \"key\": [\n 1,\n -2.5,\n \"three\"\n ]\n}",
"[\n \"deep\"\n ]",
"{\n \"a\": 1,\n \"b\": 2x\n}",
// concatenated values and whitespace
"1 2 3", R"("a""b")", "[1][2]", "{}{}", " \n\t 42 \n\t ", "",
// comments (only accepted by parse_lenient)
"/* comment */ 1", "// comment\n[1, /* c */ 2]",
// byte order mark
"\xef\xbb\xbf[1]",
};
}
} // namespace
TEST_CASE("parsing from a std::istream scans the get area in bulk")
{
SECTION("results do not depend on the get area")
{
const std::vector<read_mode> read_modes = {read_mode::extract, read_mode::parse, read_mode::parse_lenient};
for (const auto& input : corpus())
{
for (const read_mode mode : read_modes)
{
// no get area: the character-at-a-time path
const std::string expected = observe(input, 0, mode);
for (const std::size_t chunk_size : chunk_sizes())
{
CAPTURE(input)
CAPTURE(chunk_size)
CAPTURE(static_cast<int>(mode))
CHECK(observe(input, chunk_size, mode) == expected);
}
}
}
}
SECTION("values are the same as when parsing from a string")
{
const std::string input = R"({"name": "nlohmann::json", "numbers": [1, -2, 3.5, 4e10, 18446744073709551615],)"
"\n \"text\": \"caf\xc3\xa9 \\u00e9 \\\"quoted\\\"\", \"nested\": [[{}], [[]]], \"flags\": [true, false, null]}";
const json expected = json::parse(input);
for (const std::size_t chunk_size : chunk_sizes())
{
CAPTURE(chunk_size)
chunked_streambuf buf(input, chunk_size);
std::istream is(&buf);
CHECK(json::parse(is) == expected);
}
std::istringstream ss(input);
CHECK(json::parse(ss) == expected);
}
SECTION("a token that spans several get areas is reported completely")
{
// the error is found after the string has crossed many get areas; the
// token string holds everything since the start of the last token
const std::string input = R"(["this string is longer than every get area", tru])";
for (const std::size_t chunk_size : chunk_sizes())
{
CAPTURE(chunk_size)
chunked_streambuf buf(input, chunk_size);
std::istream is(&buf);
json j;
CHECK_THROWS_WITH_AS(is >> j,
"[json.exception.parse_error.101] parse error at line 1, column 50: syntax error while parsing value - invalid literal; last read: '\"this string is longer than every get area\", tru]'",
json::parse_error&);
}
const std::string unterminated = R"("this string is longer than every get area)";
for (const std::size_t chunk_size : chunk_sizes())
{
CAPTURE(chunk_size)
chunked_streambuf buf(unterminated, chunk_size);
std::istream is(&buf);
json j;
CHECK_THROWS_WITH_AS(is >> j,
"[json.exception.parse_error.101] parse error at line 1, column 43: syntax error while parsing value - invalid string: missing closing quote; last read: '\"this string is longer than every get area'",
json::parse_error&);
}
}
SECTION("an error before the first token reports everything read")
{
chunked_streambuf buf(" \n x", 2);
std::istream is(&buf);
json j;
CHECK_THROWS_WITH_AS(is >> j,
"[json.exception.parse_error.101] parse error at line 2, column 3: syntax error while parsing value - invalid literal; last read: ' <U+000A> x'",
json::parse_error&);
}
SECTION("lines and columns are counted across get areas")
{
const std::string input = "[\n 1,\n \"two\",\n 3.0,\n x\n]";
for (const std::size_t chunk_size : chunk_sizes())
{
CAPTURE(chunk_size)
chunked_streambuf buf(input, chunk_size);
std::istream is(&buf);
json j;
CHECK_THROWS_WITH_AS(is >> j,
"[json.exception.parse_error.101] parse error at line 5, column 5: syntax error while parsing value - invalid literal; last read: '3.0,<U+000A> x'",
json::parse_error&);
}
}
SECTION("many values from one stream")
{
std::string input;
for (int i = 0; i < 200; ++i)
{
input += R"({"id":)" + std::to_string(i) + R"(,"name":"user)" + std::to_string(i) + R"(","score":)" + std::to_string(i) + ".5}\n";
input += std::to_string(i * 7919) + "\n";
}
for (const std::size_t chunk_size : chunk_sizes())
{
CAPTURE(chunk_size)
chunked_streambuf buf(input, chunk_size);
std::istream is(&buf);
json j;
int count = 0;
while (!(is >> std::ws).eof())
{
is >> j;
if (count % 2 == 0)
{
CHECK(j["id"] == count / 2);
}
else
{
CHECK(j == (count / 2) * 7919);
}
++count;
}
CHECK(count == 400);
}
}
SECTION("the stream can be used after parsing")
{
for (const std::size_t chunk_size : chunk_sizes())
{
CAPTURE(chunk_size)
chunked_streambuf buf(R"({"a": "some text"} trailing words 42)", chunk_size);
std::istream is(&buf);
json j;
is >> j;
CHECK(j == json({{"a", "some text"}}));
std::string word;
int number = 0;
is >> word;
CHECK(word == "trailing");
is >> word >> number;
CHECK(word == "words");
CHECK(number == 42);
}
}
SECTION("std::stringstream and std::ifstream")
{
const std::string input = R"([1, 2.5, "three", {"four": [true, null]}] 5)";
std::stringstream ss(input);
json j1;
json j2;
ss >> j1 >> j2;
CHECK(j1 == json::parse(R"([1, 2.5, "three", {"four": [true, null]}])"));
CHECK(j2 == 5);
CHECK(ss.eof());
// a file name of its own for every run: the test runs for several C++
// standards and configurations at the same time, in the same directory
std::random_device rd;
const std::string temp_name = "unit-stream-bulk-scan-" + std::to_string(rd()) + ".tmp";
const char* const filename = temp_name.c_str();
{
std::ofstream file(filename);
file << input;
}
{
std::ifstream file(filename);
json k1;
json k2;
file >> k1 >> k2;
CHECK(k1 == j1);
CHECK(k2 == j2);
}
static_cast<void>(std::remove(filename));
}
SECTION("binary formats read from a stream")
{
const json j = {{"string", std::string(300, 'x')}, {"array", {1, 2, 3}}, {"number", 1.5}};
const std::vector<std::uint8_t> cbor = json::to_cbor(j);
const std::string bytes(cbor.begin(), cbor.end());
for (const std::size_t chunk_size : chunk_sizes())
{
CAPTURE(chunk_size)
chunked_streambuf buf(bytes, chunk_size);
std::istream is(&buf);
CHECK(json::from_cbor(is) == j);
}
}
#if TEST_STREAM_BULK_SCAN
SECTION("the adapter keeps no token unless the lexer starts one")
{
// the binary readers read through the same adapter, but never start a
// token; refilling the get area must then not collect the input
chunked_streambuf buf(std::string(1000, 'x'), 7);
std::istream is(&buf);
nlohmann::detail::input_stream_adapter ia(is);
for (int i = 0; i < 1000; ++i)
{
CHECK(ia.get_character() == 'x');
}
std::vector<char> token;
ia.copy_token(token, false);
CHECK(token.empty());
}
#endif
SECTION("SAX parsing from a stream")
{
const std::string input = R"({"a": [1, "two", 3.5]} rest)";
for (const std::size_t chunk_size : chunk_sizes())
{
CAPTURE(chunk_size)
chunked_streambuf buf(input, chunk_size);
std::istream is(&buf);
json j;
nlohmann::detail::json_sax_dom_parser<json, nlohmann::detail::string_input_adapter_type> sax(j);
CHECK(json::sax_parse(is, &sax, json::input_format_t::json, false));
CHECK(j == json::parse(R"({"a": [1, "two", 3.5]})"));
}
}
}