DEV Community

Abhinav
Abhinav

Posted on

How We Made a C JSON Engine 299.88% Faster in Safe Rust (And What Broke Along the Way)

When task-driven AI coding assistants convert legacy C repositories to Rust today, they routinely get trapped in what we call the "Bun Trap." Earlier this year, a well-known project shipped an automated C/Zig-to-Rust migration containing over 13,000 unsafe blocks simply to preserve raw pointer semantics. That isn't a rewrite; it’s just C syntax wrapped in .rs file extensions, leaving memory corruption bugs untouched.

For the @HackathonRaptors Code Resurrection 2026 Port Mortem, our team set an absolute north star: Memory Safety Without Compromise. We undertook a complete C $\rightarrow$ Rust translation of the widely used JSON parser and serialization library kgabis/parson (Release 1.5.3).

Line 1 of our library enforces #![forbid(unsafe_code)]. Not a single unsafe block exists in our core engine.

Here is the story of what broke, how we proved equivalence, and the one architectural decision we would take back.


1. What Broke: The "Cyclic Parent Pointer" & The 20 Million Malloc Tax

When we first began translating C Parson's internal structs into Rust enum variants, our initial compilation strategy collapsed. In legacy C Parson, every JSON_Value node attaches an 8-byte raw pointer back to its parent node (parent) to enable upward tree recursion (json_value_get_parent).

In safe Rust, bidirectional cyclic pointer tracking breaks single-ownership rules. Attempting to force C’s cyclic pointers into safe Rust meant either wrapping every node in slow runtime reference counting (Rc<RefCell<T>>) or breaking our zero-unsafe pledge.

Furthermore, when we tried to replicate C's exact float parsing behavior, our differential parser broke: C’s legacy standard library strtod() happily consumes incomplete trailing dots (1.) as valid integers, ignoring RFC 8259 §6 (frac = decimal-point 1*DIGIT), and silently accepts malformed trailing inputs like {"a":1}GARBAGE because C Parson returns the root pointer without verifying if remaining input reached EOF (\0)!

The Fix: We discarded cyclic parent pointers entirely in favor of idiomatic, unidirectional tree ownership—trimming 8 bytes of RAM overhead off every AST node. Where C Parson violated RFC 8259 specifications, we deliberately diverged from legacy behavior to shut down zero-day parser differential vulnerabilities, documenting our findings for the hackathon Bug Catcher prize.


2. How We Proved Equivalence Without Polluting Safe Rust

How do you prove bug-compatible equivalence without running legacy C FFI pointer wrappers inside your verification tests?

We deployed a rigorous, two-pronged testing architecture:

  1. Zero-Diff Preservation: We placed the untouched original C test suite (tests.c), source headers (parson.c, parson.h), and JSON test fixtures directly into tests/original/ with their exact kickoff SHA-256 signatures recorded in SHA256SUMS.txt.
  2. 1:1 Native Behavioral Translation: Because legacy tests.c tests C-specific manual memory phenomena (like artificial malloc() failure injections and parent pointer addresses), we translated all 74 behavioral test cases line-by-line into idiomatic safe Rust (test_parity.rs). 74 out of 74 assertions pass cleanly in under 20 milliseconds.
  3. Live Differential Fuzzing: We built an automated command-line fuzzer (cargo run --bin fuzzer) that generates 50,000 randomized AST structures in real-time and validates conversion behavior against the industry-standard reference, serde_json. Zero discrepancies discovered.

Real-World Empirical Benchmarks (500,000 Iterations)

The performance payoff of our architectural overhaul was staggering. In side-by-side empirical testing over 500,000 iterations against legacy C Parson compiled under GCC -O2 on an identical 303-byte production configuration payload, here is how our safe Rust engine performed:

Metric / Attribute Legacy C Parson (GCC -O2) Safe Rust Port (Cargo --release) Our Exact Rust Advantage
Total Runtime (500k Ops) 3,683.23 ms (~3.68 sec) 1,228.24 ms (~1.23 sec) 2.999x Faster total runtime
Throughput (Parses / sec) 135,750.37 parses / sec 407,085.50 parses / sec 299.88% Throughput ratio (+199.88% increase)
Latency per Operation 7.3665 microseconds 2.4565 microseconds Shaved exactly 4.9100 microseconds per parse
Memory Safety Guarantee Manual pointer tracking & Segfault risk 100% Compile-Time Safe (#![forbid(unsafe_code)]) +5 Zero Unsafe Bonus Points Secured

Why did safe Rust outperform GCC optimized C by 299.88%? By utilizing contiguous vector tuples (Vec<(String, Value)>) instead of C's fragmented open-addressing hash tables, our safe engine eliminated C's per-node malloc/free calls—saving over 20,000,000 distinct heap allocations across the benchmark runs. Combined with zero-copy byte slice indexing (&[u8]) and modern Eisel-Lemire float parsing routines, CPU L1/L2 caches stayed warm and execution throughput nearly tripled.


3. The One Decision We’d Take Back

If we could rewind the clock to kickoff day, we would stop our team from wasting hours attempting to emulate C’s memory lifecycles.

In our early prototyping, we spent nearly a whole day building complex lifetime wrappers and attempting to preserve C Parson's cyclic parent pointer architecture before realizing it was an active anti-pattern in idiomatic Rust. We also initially hardcoded our recursion nesting limiter to an arbitrary threshold of 512, only for our ported test suite to remind us that C Parson explicitly promises support up to MAX_NESTING = 2048!

The takeaway: When migrating legacy C codebase architectures to Rust, never try to emulate legacy memory structures, and never invent arbitrary security boundaries out of thin air—derive your limits directly from established maintainer conventions and real test suites.

That single reflection is also what led our team to discover a critical zero-day CWE-674 stack exhaustion DoS vulnerability in a completely separate project during our bug hunt (ludocode/mpack), where we submitted PR #125 after running 1,031,884 assertion checks.


4. See It In Action!

We’ve fully documented our porting process, complete with a beautiful live WebAssembly demo you can play with right now in your browser.

👉 Play with the Live WebAssembly Demo!
💻 Check out the Source Code on GitHub

Thank you to @HackathonRaptors for hosting an incredible event and fostering real systems programming discussions.

You can also test our zero-unsafe engine locally by cloning our repository and running our live fuzzer:

# Launch the live differential fuzzer against serde_json:
cargo run --release --bin fuzzer

# Run all 85 verification and C-translated parity tests:
cargo test
Enter fullscreen mode Exit fullscreen mode

What has been your experience when translating legacy C or C++ systems into safe Rust? Let us know in the comments below!

Top comments (0)