# RustyJson Architecture

This document explains the architectural decisions behind RustyJson and compares them to Jason's approach.

## Overview

RustyJson is a Rust NIF-based JSON library that prioritizes **memory efficiency** over theoretical purity. While Jason returns true iolists, RustyJson returns single binaries - a deliberate trade-off that yields 10-20x memory reduction during encoding.

## Encoding Architecture

### Jason's Approach (Pure Elixir, True iolist)

```
Elixir Term
    │
    ▼
Protocol Dispatch (Jason.Encoder)
    │
    ▼
Build iolist incrementally
["{", ['"', "key", '"'], ":", "1", "}"]
    │
    ▼
Return iolist (many small binaries)
```

**Characteristics:**
- Many small BEAM binary allocations
- Each struct/map/list creates nested list structures
- BEAM owns all memory from the start
- Can theoretically stream to sockets without flattening

### RustyJson's Approach (Rust NIF, Single Binary)

```
Elixir Term
    │
    ▼
Optional: Protocol Dispatch (RustyJson.Encoder)
    │
    ▼
NIF Call ──────────────────────────┐
    │                              │
    ▼                              │ Rust
Walk term tree directly            │
Write to single buffer (mimalloc)  │
    │                              │
    ▼                              │
Copy buffer to BEAM binary ────────┘
    │
    ▼
Return single binary
```

**Characteristics:**
- One buffer allocation in Rust
- No intermediate Elixir terms created
- Single copy from Rust → BEAM at the end
- Must complete encoding before returning

## Memory Comparison

For a 10MB JSON payload:

| Phase | Jason | RustyJson |
|-------|-------|-----------|
| Input term | 10 MB | 10 MB |
| During encoding | +140 MB (intermediate binaries) | +5 MB (Rust buffer) |
| Peak memory | ~150 MB | ~15 MB |
| Final output | 10 MB iolist | 10 MB binary |

The dramatic difference comes from Jason creating many intermediate binary allocations during the recursive encoding process.

## Safety Analysis

### The NIF Safety Question

A common concern with NIFs is: "What if it crashes and takes down my entire BEAM VM?"

This is a valid concern for C-based NIFs, where a single buffer overflow, null pointer dereference, or use-after-free can crash the VM. However, **RustyJson's architecture makes this effectively impossible**.

### Why RustyJson Cannot Crash the VM

#### 1. Rust's Compile-Time Memory Safety

Rust eliminates entire categories of bugs at compile time:

| Bug Category | C/C++ NIF | Rust NIF | How Rust Prevents |
|--------------|-----------|----------|-------------------|
| Null pointer dereference | Runtime crash | Won't compile | `Option<T>` forces handling |
| Buffer overflow | Runtime crash | Won't compile | Bounds checking enforced |
| Use after free | Runtime crash | Won't compile | Ownership system |
| Double free | Runtime crash | Won't compile | Single owner rule |
| Data races | Undefined behavior | Won't compile | Send/Sync traits |
| Uninitialized memory | Undefined behavior | Won't compile | All values initialized |

These aren't runtime checks that could be bypassed—the Rust compiler literally refuses to produce code with these bugs.

#### 2. Rustler's Safety Layer

[Rustler](https://github.com/rusterlium/rustler) adds BEAM-specific protections:

- **Panic catching**: Rust panics are caught via `std::panic::catch_unwind` and converted to Elixir exceptions. A panic cannot crash the VM.
- **Term safety**: All Erlang term manipulation goes through safe APIs that validate types.
- **No raw pointers**: Rustler's `Term` type wraps raw BEAM pointers safely.
- **Lifetime enforcement**: Rust's borrow checker ensures terms aren't used after their scope ends.

#### 3. Zero `unsafe` Code in Our Codebase

**RustyJson's source code contains zero `unsafe` blocks.** Not minimized, not
audited—literally zero. Every line of Rust we wrote is 100% safe Rust.

```rust
// All our code uses safe patterns like this:
fn peek(&self) -> Option<u8> {
    self.input.get(self.pos).copied()  // Safe: returns None if out of bounds
}

fn skip_whitespace(&mut self) {
    while let Some(&byte) = self.input.get(self.pos) {  // Safe: bounds-checked
        match byte {
            b' ' | b'\t' | b'\n' | b'\r' => self.pos += 1,
            _ => break,
        }
    }
}
```

**Our source files:**

| File | Purpose | Lines | Unsafe |
|------|---------|-------|--------|
| `lib.rs` | NIF entry point | ~100 | **0** |
| `direct_json.rs` | JSON encoder | ~1,000 | **0** |
| `direct_decode.rs` | JSON decoder | ~500 | **0** |
| `compression.rs` | Gzip compression | ~60 | **0** |
| `decimal.rs` | Decimal handling | ~110 | **0** |
| **Total** | | **~1,770** | **0** |

**Design choices that eliminate unsafe:**

1. **`&str` over `&[u8]`** - UTF-8 validity guaranteed at compile time
2. **`.get()` over indexing** - Returns `Option` instead of panicking
3. **`while let Some()`** - Idiomatic safe iteration pattern

**Where `unsafe` does exist (dependencies only):**

| Dependency | Purpose | Trust Level |
|------------|---------|-------------|
| Rustler | NIF ↔ BEAM bridge | High - 10+ years, 500+ projects |
| mimalloc | Memory allocator | High - Microsoft, battle-tested |
| Rust stdlib | Core functionality | Highest - Rust core team |

Any memory safety bug would have to originate in a dependency, not in RustyJson code.

#### 4. Explicit Resource Limits

We enforce limits that prevent resource exhaustion:

| Resource | Limit | Rationale |
|----------|-------|-----------|
| Nesting depth | 128 levels | Prevents stack overflow, per RFC 7159 |
| Recursion | Bounded by depth | No unbounded recursion possible |
| Allocation | System memory | Same as pure Elixir |

### What Would Actually Need to Go Wrong?

For RustyJson to crash the VM, one of these would need to happen:

| Scenario | Likelihood | Why It's Unlikely |
|----------|------------|-------------------|
| Bug in Rustler | Extremely low | Mature library, used in production by many projects |
| Bug in Rust compiler | Essentially zero | Rust compiler is formally verified for memory safety |
| Bug in mimalloc | Extremely low | Microsoft's allocator, battle-tested |
| Cosmic ray bit flip | Non-zero but... | Not a software problem |

None of these are RustyJson bugs—they're infrastructure bugs that would affect any Rust-based system.

### Comparison: Theoretical vs Practical Risk

| Aspect | Theoretical Risk | Practical Risk |
|--------|------------------|----------------|
| Memory corruption | "NIFs can crash" | Rust prevents at compile time |
| Stack overflow | "Deep recursion" | 128-depth limit enforced |
| Panic/exception | "Unhandled errors" | Rustler converts to Elixir errors |
| Scheduler blocking | "Long NIF calls" | JSON encoding is fast, bounded |

### Our Position

We consider RustyJson **as safe as pure Elixir code** for the following reasons:

1. **Safe Rust is memory-safe by construction**. The compiler guarantees it.

2. **Rustler has been production-tested** across hundreds of Elixir projects for years.

3. **We use no `unsafe` code** in our encoding/decoding logic.

4. **Panics become exceptions**, not crashes.

5. **Resource limits are enforced** (depth, recursion).

The "NIFs can crash your VM" warning applies to **C-based NIFs** where a single bug can corrupt memory. It does not meaningfully apply to safe Rust NIFs, which have the same memory safety guarantees as the BEAM itself.

### When to Consider Alternatives

If you require defense-in-depth for untrusted input, you could:

1. **Run encoding in a Task**: Crashes isolated to that process
2. **Use Jason for untrusted input**: Pure Elixir, cannot crash VM
3. **Add pre-validation**: Check input structure before encoding

However, we believe these are unnecessary for RustyJson given the safety guarantees above.

### Garbage Collection

| Aspect | Jason | RustyJson |
|--------|-------|-----------|
| Allocation pattern | Many small binaries | One large binary |
| GC type | Regular + refc | Refc only |
| GC timing | Incremental | Atomic release |
| Memory spike | During encoding | Brief, at copy |

**Refc binaries** (>64 bytes) are reference-counted and stored outside the process heap. RustyJson always produces refc binaries for non-trivial output.

## Performance Characteristics

### When Jason Wins

1. **Tiny payloads (<100 bytes)**: NIF call overhead exceeds encoding time
2. **Streaming scenarios**: True iolists can be sent to sockets incrementally
3. **Partial failure recovery**: Failed encodes don't leave large allocations

### When RustyJson Wins

1. **Medium payloads (1KB-1MB)**: 3-6x faster, 10-20x less memory
2. **Large payloads (1MB+)**: Memory efficiency becomes critical
3. **High-throughput APIs**: Less GC pressure, more consistent latency
4. **Memory-constrained environments**: Lower peak memory usage

### Real-World Performance

In production workloads (Amazon settlement reports):

| Metric | Jason | RustyJson | Improvement |
|--------|-------|-----------|-------------|
| Encode time (13K rows) | 1,556 ms | 70 ms | 22x faster |
| Memory during encode | +146.8 MB | +6.7 MB | 22x less |

## Protocol Architecture

### Default Path (Maximum Performance)

```
RustyJson.encode!(data)
        │
        ▼
    Rust NIF
        │
        ▼
  Direct term walking
  (no Elixir preprocessing)
```

No Elixir code runs during encoding. The Rust NIF walks the term tree directly.

### Protocol Path (Custom Encoding)

```
RustyJson.encode!(data, protocol: true)
        │
        ▼
RustyJson.Encoder.encode/1
        │
        ▼
  Protocol dispatch
  (Map, List, Tuple, Any, custom)
        │
        ▼
    Rust NIF
```

When `protocol: true`:
1. Elixir's protocol system preprocesses the data
2. Custom `RustyJson.Encoder` implementations are called
3. Structs without implementations are converted via `Map.from_struct()`
4. Preprocessed data is sent to Rust

## Decoding Architecture

### Jason

```
JSON String
    │
    ▼
Recursive descent parser (Elixir)
    │
    ▼
Build Elixir terms during parse
    │
    ▼
Return term
```

### RustyJson

```
JSON String
    │
    ▼
NIF Call ──────────────────────────┐
    │                              │
    ▼                              │ Rust
Custom parser                      │
Build Erlang terms via Rustler     │
Zero-copy strings when possible    │
    │                              │
    ▼ ─────────────────────────────┘
Return term (already on BEAM heap)
```

Decoding builds terms directly on the BEAM heap via Rustler's term API, avoiding intermediate Rust allocations.

## Fragment Architecture

Fragments allow injecting pre-encoded JSON:

```
%RustyJson.Fragment{encode: ~s({"pre":"encoded"})}
        │
        ▼
    Rust NIF
        │
        ▼
  Detect Fragment struct
  Write iodata directly to buffer
  (no re-encoding)
```

This is critical for:
- PostgreSQL `jsonb_agg` results
- Cached JSON responses
- Third-party API proxying

## Design Decisions

### Why Single Binary Instead of iolist?

1. **Memory efficiency**: The 10-20x memory reduction during encoding far outweighs the theoretical benefits of iolists.

2. **Phoenix flattens anyway**: `Plug.Conn` typically calls `IO.iodata_to_binary/1` before sending responses.

3. **Simpler NIF interface**: Returning a single binary is cleaner than building nested Erlang lists in Rust.

4. **Predictable performance**: Single allocation is easier to reason about than many small ones.

### Why Optional Protocol?

1. **Maximum default performance**: Most data doesn't need custom encoding.

2. **Explicit opt-in**: Users consciously trade performance for flexibility.

3. **Custom encoding**: Supports `@derive RustyJson.Encoder` for struct field filtering.

### Why 128-Level Depth Limit?

1. **RFC 7159 compliance**: The spec recommends implementations limit nesting.

2. **Stack safety**: Prevents stack overflow in recursive encoding/decoding.

3. **DoS protection**: Malicious deeply-nested JSON can't exhaust resources.

## Future Considerations

### Potential Optimizations

1. **SIMD parsing**: Libraries like simd-json could accelerate decoding, though term construction dominates.

2. **Chunked output**: For 100MB+ payloads, returning iolists could reduce memory spikes.

3. **Streaming decode**: Parse JSON incrementally for very large inputs.

### Not Planned

1. **True iolist output**: The complexity isn't justified by real-world benefits.

2. **Unsafe Rust**: Memory safety is non-negotiable.

3. **Custom allocators per-call**: mimalloc is fast enough globally.

## References

- [Rustler](https://github.com/rusterlium/rustler) - Safe Rust NIFs for Erlang/Elixir
- [Jason](https://github.com/michalmuskala/jason) - Reference implementation
- [RFC 8259](https://tools.ietf.org/html/rfc8259) - JSON specification
- [mimalloc](https://github.com/microsoft/mimalloc) - Memory allocator
