Skip to content

Add JSON encoding with simdjson 4.6.4#112

Open
jlambatl wants to merge 3 commits into
FourierTransformer:masterfrom
jlambatl:encode-string-builder-v2
Open

Add JSON encoding with simdjson 4.6.4#112
jlambatl wants to merge 3 commits into
FourierTransformer:masterfrom
jlambatl:encode-string-builder-v2

Conversation

@jlambatl

Copy link
Copy Markdown
Contributor

This supersedes [#109] with a focused implementation based directly on the current master branch. It incorporates the review feedback from the original PR while removing unrelated formatting, benchmark, security-test, and CI changes. This updated PR includes changes uncovered through internal reviews.

Summary

This adds JSON encoding using simdjson’s string_builder:

simdjson.encode(value[, {
    maxDepth = 128,
    bufferSize = 16384
}])

The following configuration functions are also provided:

simdjson.setMaxEncodeDepth(value)
simdjson.getMaxEncodeDepth()
simdjson.setEncodeBufferSize(value)
simdjson.getEncodeBufferSize()

The optional argument is an options table, so additional encoding options can be introduced later without changing the function signature.

Encoding behaviour

  • Strings, numbers, booleans, tables, nil, and simdjson.null are supported.
  • Lua integers are preserved on Lua 5.3 and newer.
  • Other numbers use simdjson’s shortest valid floating-point representation.
  • Numeric object keys use the same simdjson number formatting.
  • Dense tables with consecutive integer keys from 1 through n are encoded as arrays.
  • Sparse and mixed-key tables are encoded as objects, avoiding potentially enormous sparse-array output.
  • Embedded NUL bytes in strings are preserved.
  • Encoded strings are validated as UTF-8.
  • NaN and infinity are rejected because they are not valid JSON numbers.
  • Direct and indirect table cycles produce an explicit error.
  • Unsupported values and unknown or invalid options produce explicit errors.
  • Maximum encoding depth is capped at 128 to protect the native stack.
  • Initial encoder buffer allocation is capped at 64 MiB. The buffer can still grow as required while producing output.

Thread-local state and buffer ownership

The reusable simdjson parser, padded parse buffer, and encoder string builder are now thread-local. This prevents mutable parser and builder state from being shared across threads.
The encoder buffer is owned by a thread-local std::unique_ptr, ensuring it is released when the thread exits rather than leaking a raw allocation.
Lua-owned strings don't guarantee the trailing padding required by simdjson. The previous page-boundary heuristic could therefore allow simdjson to read beyond the Lua allocation. Parsing now copies input into a reusable thread-local padded buffer with explicit overflow and allocation checks. The allocation is retained and reused by subsequent parses on the same thread.

simdjson 4.6.4 and C++17

The vendored simdjson amalgamation has been updated from 4.2.4 to the official simdjson 4.6.4 release. This version includes the string-builder overflow fix used by the encoder. The vendored files were verified against the official release artifacts.

Unix, MinGW, and MSVC builds now consistently require C++17.

Previous PR Review feedback addressed

  • Booleans and numbers are appended directly using string_builder::append.
  • Lua integers use the integer overload, which means the Lua runtime supports a distinct integer type.
  • Floating-point values use the simdjson formatter instead of custom formatting.
  • New C++ functions use snake_case naming.
  • The implementation is isolated in lua_encoder.cpp and lua_encoder.h.
  • The public Lua API retains the established camelCase naming.
  • Unrelated whole-file formatting and bulk benchmark changes from Initial implementation of encode() using simdjson's string_builder #109 are not included.

Tests and CI

Focused tests cover:

  • Scalar, nested, and array booleans.
  • Lua integers and floating-point formatting across Lua versions.
  • Strings, embedded NUL bytes, and UTF-8 validation.
  • Nulls, arrays, objects, sparse tables, and numeric object keys.
  • Per-call and global configuration.
  • Buffer growth beyond the initial capacity.
  • Maximum nesting depth and configuration overflow.
  • NaN, infinity, unsupported values, and cyclic tables.
  • Recovery after encoding errors.
  • 1,000 deterministic randomised acyclic values, with every encoded result parsed again to verify valid JSON.
  • A dedicated Linux CI job builds and runs the suite with AddressSanitizer and UndefinedBehaviorSanitizer. This sanitizer coverage identified the unsafe parser-padding assumption described above.

The complete 35-job GitHub Actions matrix passes across:
Lua 5.1–5.5.
LuaJIT 2.0 and 2.1.
Linux.
macOS Intel and ARM64.
Windows with MinGW and MSVC.
ASan and UBSan.
The tests were authored as part of this implementation. Copilot was used only to assist with Busted boilerplate. Codex was used to review the code one more time before we moved it from our fork and committed it back to the community.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant