Skip to content

Latest commit

 

History

History
290 lines (211 loc) · 10.8 KB

File metadata and controls

290 lines (211 loc) · 10.8 KB

lite3-zig

WARNING: This library is fully vibe-coded; it's just for me for now. use at your own risk

(Human's note: aim's to be) Idiomatic Zig wrapper for Lite³, a JSON-compatible zero-copy serialization format that encodes data as a B-tree inside a single contiguous buffer, allowing O(log n) access and mutation on any arbitrary field.

Features

  • Buffer API — fixed-size, caller-managed memory; ideal for embedded, real-time, or arena-based workflows.
  • Context API — heap-allocated, auto-growing buffer via lite3's internal C allocator.
  • ManagedContext API — allocator-explicit, auto-growing buffer managed by Zig allocator.
  • ExternalContext API — no stored allocator; provide allocator only to growth-capable calls.
  • Full type support — null, bool, i64, f64, string, bytes, nested objects, and arrays.
  • JSON round-trip — encode to / decode from JSON via the bundled yyjson backend.
  • Iteration — iterate over object keys or array elements.
  • Proper error handling — all C error codes are translated to Zig error unions.
  • Flexible keys — accepts []const u8 keys (no sentinel terminator required; embedded \0 is rejected).
  • Zero @cImport issues — a thin C shim wraps the inline functions that Zig's translate-c cannot handle (alignment casts, flexible array members, GNU statement expressions).

Requirements

  • Zig ≥ 0.15.2
  • A C11-capable toolchain (provided by Zig)

The C source for lite3 is vendored directly in vendor/lite3/ — no submodules needed.

Quick start

const lite3 = @import("lite3");

// Using the Context API (auto-growing buffer via C malloc/free)
var ctx = try lite3.Context.init();
defer ctx.deinit();

try ctx.resetObj();
try ctx.setStr(lite3.root, "name", "Alice");
try ctx.setI64(lite3.root, "age", 30);

const name = try ctx.getStr(lite3.root, "name");
// name == "Alice"

// Encode to JSON
const json = try ctx.jsonEncode(lite3.root);
defer json.deinit();
const std = @import("std");
const lite3 = @import("lite3");

// Using the ManagedContext API (auto-growing via Zig allocator)
var mctx = try lite3.ManagedContext.init(std.heap.page_allocator);
defer mctx.deinit();

try mctx.resetObj();
try mctx.setStr(lite3.root, "name", "Alice");
const std = @import("std");
const lite3 = @import("lite3");

// Using ExternalContext (no allocator stored in the type)
var ectx = try lite3.ExternalContext.init(std.heap.page_allocator);
defer ectx.deinit(std.heap.page_allocator);

try ectx.resetObj();
try ectx.setStr(std.heap.page_allocator, lite3.root, "name", "Alice");
// Using the Buffer API (fixed-size, caller-managed memory)
var mem: [4096]u8 align(4) = undefined;
var buf = try lite3.Buffer.initObj(&mem);

try buf.setI64(lite3.root, "answer", 42);
const val = try buf.getI64(lite3.root, "answer");
// val == 42

Building

# Build the static library
zig build

# Run all tests
zig build test

# Build with specific optimization
zig build -Doptimize=ReleaseFast

Build options

Option Default Description
-Djson=false true Disable JSON backend; JSON APIs return error.InvalidArgument
-Derror-messages false Enable lite3 debug error messages to stdout
-Dlto=true false Currently unsupported (build fails fast with a clear message)

Building examples

zig build examples

Running benchmarks

zig build bench

Using as a dependency

Add this package to your build.zig.zon:

.dependencies = .{
    .lite3 = .{
        .url = "https://github.com/<your-fork>/lite3-zig/archive/<commit>.tar.gz",
        .hash = "...",
    },
},

Then in your build.zig:

const lite3_dep = b.dependency("lite3", .{
    .target = target,
    .optimize = optimize,
});
exe.root_module.addImport("lite3", lite3_dep.module("lite3"));

Examples

See the examples/ directory for standalone example programs:

  • basic.zig --- Creating documents, setting/getting values, nested objects and arrays, iteration
  • json_roundtrip.zig --- JSON encode/decode round-trips with both Buffer and Context APIs

API overview

Types

Zig type Description
Buffer Fixed-size buffer with caller-managed memory
Context Heap-allocated, auto-growing buffer (C allocator)
ManagedContext Auto-growing buffer managed by Zig allocator
ExternalContext Auto-growing buffer; allocator passed per grow-capable call
Type Enum of value types (null, bool_, i64_, f64_, etc.)
Offset Typed offset handle into the buffer (enum(usize))
Error Error set (NotFound, InvalidArgument, etc.)
Value Tagged union for dynamic access (null, bool_, i64_, etc.)
JsonString Opaque handle to C-allocated JSON; freed via .deinit()
Buffer.Iterator Iterator over object/array entries

Buffer API

Method Description
initObj / initArr Initialize as object or array
setNull/Bool/I64/F64/Str/Bytes/Obj/Arr Set a value by key
getBool/I64/F64/Str/Bytes/Obj/Arr Get a value by key
getType / exists Query type or existence of a key (Error!)
getValue Get value as a Value tagged union
getStrCopy / getBytesCopy Copy string/bytes into caller buffer (safe)
arrAppend* Append values to an array
arrGet* Get values from an array by index
arrGetStrCopy / arrGetBytesCopy Copy array string/bytes into caller buffer
count Count entries in an object or array
iterate Create an iterator
jsonDecode Decode JSON into a buffer
jsonEncode Encode buffer contents to JSON (JsonString)
jsonEncodePretty Encode to pretty-printed JSON (JsonString)
jsonEncodeBuf Encode JSON into a caller-supplied buffer

Context API

The Context API manages memory automatically via lite3's internal C allocator (malloc/free) and exposes the same value/query/array methods as Buffer. Lifecycle methods are context-specific:

  • init / initWithSize / initFromBuf to construct a context
  • resetObj / resetArr to reset the root container type
  • deinit to release resources (idempotent)

ManagedContext API

ManagedContext mirrors the operational API of Context, but allocation is explicit through a caller-provided Zig allocator:

  • init(allocator) / initWithCapacity(allocator, n) / initFromBuf(allocator, buf)
  • Mutating operations auto-grow on Error.NoBufferSpace
  • deinit releases allocator-owned memory (idempotent)

ExternalContext API

ExternalContext also uses Zig allocators, but it does not store one internally:

  • init(allocator) / initWithCapacity(allocator, n) / initFromBuf(allocator, buf)
  • Pass allocator only to grow-capable operations (set*, arrAppend*, importFromBuf, jsonDecode)
  • deinit(allocator) requires the same allocator used for init/growth

Project structure

lite3-zig/
├── build.zig           # Zig build system
├── build.zig.zon       # Package metadata
├── Justfile            # Task automation
├── .mise.toml          # Dev environment (Zig 0.15.2)
├── src/
│   ├── lite3.zig       # Zig wrapper module
│   ├── lite3_shim.c    # C shim for inline functions
│   ├── lite3_shim.h    # C shim header
│   ├── lite3_json_disabled.c # JSON stubs for -Djson=false
│   ├── bench.zig       # Benchmarks
│   └── tests.zig       # Comprehensive test suite
├── examples/
│   ├── basic.zig          # Basic usage example
│   └── json_roundtrip.zig # JSON encode/decode example
└── vendor/
    └── lite3/          # Vendored upstream sources (github.com/fastserial/lite3)

Development

# With mise and just installed:
mise install          # Install Zig 0.15.2
just test             # Run tests
just test-release     # Run tests with ReleaseSafe
just test-no-json     # Run tests with JSON backend disabled
just clean            # Remove build artifacts
just update-vendor    # Update vendored lite3 sources
just act-local        # Run local CI in act

Local GitHub Actions (act)

To run CI locally, use the dedicated workflow in .github/workflows/ci-local.yml:

act workflow_dispatch -W .github/workflows/ci-local.yml

If you install act and want a short alias, use just act-local from the root of this repository.

The local workflow intentionally mirrors the existing CI commands on Linux so it can run in act consistently without requiring a macOS runner.

Safety notes

Dangling pointers

Methods that return string or byte slices (getStr, getBytes, arrGetStr, arrGetBytes) return pointers directly into the underlying buffer. These slices are invalidated by:

  • Any mutation to the same buffer (Buffer API)
  • Any mutation to the context (Context API), since the context may reallocate its internal buffer

To safely retain a value across mutations, use the copy variants:

var dest: [256]u8 = undefined;
const safe = try buf.getStrCopy(lite3.root, "key", &dest);
// safe remains valid after mutations

The copy variants are: getStrCopy, getBytesCopy, arrGetStrCopy, arrGetBytesCopy.

Thread safety

Buffer and Context are not thread-safe. Concurrent reads and writes require external synchronization (e.g. a Mutex). Iterators are also invalidated by any mutation.

Architecture notes

C shim layer

Lite³ makes heavy use of GNU C extensions (statement expressions, __builtin_prefetch) and C patterns (flexible array members, volatile casts) that Zig's translate-c cannot handle. Rather than patching the upstream library, a thin C shim (src/lite3_shim.c) wraps every inline function as a proper extern function, giving Zig clean function pointers to call.

Build optimization

The C library is always compiled with -OReleaseFast regardless of the Zig optimization level. This is because lite3 uses intentional out-of-bounds __builtin_prefetch hints for performance that would trigger false positives under Zig's Debug-mode bounds checking. The Zig wrapper code itself respects the user's chosen optimization level.

License

MIT License. See LICENSE for details.