Appendix B Exercise Solutions and Implementation Hints

Book contents · Previous: Appendix A Type Codes and Byte Equations · Next: Appendix C Reading the Official Implementation

Appendix B of FlatBuffers & FlexBuffers: From Bytes to Mutable Memory. Exercise Solutions and Implementation Hints from the complete supplied first edition.

This is the supplied first edition, pinned to its September 2026 source baseline. The laboratories and LiveFlex capstone are educational implementations and design proposals. The book overview explains the evidence limits; Reaktor’s performance review documents the current framework.

Appendix B: Exercise solutions and implementation hints

Solutions 1–4: foundations

1. The service avoided constructing a conventional object tree merely to read ten fields. It still acquired bytes, copied them into ownership, verified them, and performed access work. Measure acquisition/copy, verification, the ten-field traversal, and any later materialization separately; also measure their sum in the real request path. Retain the verified owner during traversal.

2. 80 is unsigned 128 or signed -128 at one byte. 80 00 is signed positive 128 at two bytes. The sign bit is bit 7 in the first signed interpretation and bit 15 in the second. Width changes the interpretation, even though the least significant byte is the same.

3. First establish position <= 100. Then 100 - position is a valid remaining extent and comparing the requested length with it cannot overflow. Adding an arbitrary large length to position first can wrap and make an invalid range appear small.

4. One reasonable design makes VerifiedBuffer own an immutable allocation. Its views borrow it and cannot outlive it. Moving the owner can preserve views only if the allocation itself stays stable and the contract says so. For independently long-lived views, retain shared ownership explicitly. Do not expose mutable bytes through the verified owner without invalidating its proof.

Solutions 5–10: FlatBuffers

5. The vtable is at 64 - (-16) = 80. The field is at 64 + 12 = 76. Both displacements are interpreted relative to the table, but one locates the vtable by signed subtraction and the other locates a field by adding the vtable entry's unsigned displacement.

6. A same-length replacement changes only the three content bytes when the allocation is writable and alias/concurrency rules permit it. A longer replacement needs enough content space, a correct length and terminator, and valid references from every affected slot. Rebuilding is the straightforward general solution. Shared string references make in-place changes visible to every alias.

7. Use a root table for the stable header and optional fields. Use a fixed-layout point struct inside a vector for dense numeric points when its schema is stable. Store dynamic metadata as an explicitly designated FlexBuffers byte payload. Verify the outer table and vector, then verify the inner FlexBuffer under its own profile; retain the outer allocation while borrowing inner bytes.

8. For an eight-byte offset slot the construction formula is S + 8 - D, after alignment. D is measured from the end during construction. The emitted distance is measured forward from the final slot. They are different coordinate systems, related by the formula rather than interchangeable values.

9. A new reader sees 5 in old buffers where the field was absent. To preserve old meaning, keep the old default and add a new field or explicit versioned interpretation, or migrate stored data deliberately. Do not assume absence contains a historical copy of the old default value.

10. Extra bytes at the end do not create a field displacement in the table's vtable or a suitable inline slot in its body. Updating shared vtable metadata can also affect other tables. A general structural edit must create a coherent table layout and references, not merely find unused allocation capacity.

Solutions 11–15: FlexBuffers primitives and vectors

11. The final byte says the root slot is one byte. The preceding 0x68 gives type 26 and child width code 0. The slot contains 1, so the value is true. A packed width code of 3 means 1 << 3 = 8; the final trailer instead stores the actual width, where 3 is not one of the permitted values.

12. Root offset: one byte. Vector count: two bytes. String offset: two bytes because it is a vector element slot. String length: one byte because the string tag describes a one-byte prefix. These four answers are the simplest diagnostic for whether the two-width model has been understood.

13. A 256-element variable typed boolean vector needs a two-byte count, so its common element width is at least two. Its slots alone occupy 512 bytes. Under the companion's straightforward layout, the complete document is 518 bytes: two count bytes, 512 element bytes, a two-byte root offset, and two trailer bytes. It is not a bitset.

14. A 300-byte blob has a two-byte count and exactly 300 payload bytes. A 300-element typed unsigned vector of byte-sized values uses two-byte slots because its count is 300, producing 600 payload bytes. In the straightforward complete layouts, the totals are 306 and 606 bytes respectively. The blob's width describes its prefix, not its content stride.

15. The untyped triple is ten bytes. Removing its three element tags produces a seven-byte typed vector. Encoding the length in a fixed tuple type removes one count byte, producing six bytes. The existing typed-vector type family is a defined set of wire codes, not a generic formula that can be extended without coordinating a new protocol assignment and reader support.

Solutions 16–20: maps, builders, mutation, verification

16. Read root width at 20 and map tag at 19. Read the root offset at 18 to reach value data at 14. Recover value count at 13, key-vector width at 12, and its offset at 11 to reach key data at 9. Read key count at 8. The foo key offset at 10 reaches position 4. Its matching index is 1, so read value slot 15 and tag 17. All widths happen to be one in this fixture; that coincidence must not become an implementation assumption.

17. get_signed() should reject all non-signed types and distinguish absence from a present null. convert_to_signed() can accept an unsigned integer only when it fits, and a float only under explicit integral/range/rounding rules. A missing key remains absence, not zero. A null conversion should be rejected unless a separate application policy intentionally supplies a default.

18. Restore byte length, temporary value/field stacks, open-container state, finished state, and every pool/index entry referring to newly written objects. Restore duplicate/error tracking according to its contract. A simpler implementation stages pool changes until commit or discards the entire failed builder operation scope.

19. Inline: 8 count bytes + 800 slots + 100 tags = 908 bytes before the root. The root distance is 900, requiring two bytes; with its type and width trailer the total is 912. Indirect: 8 scalar bytes + 1 count + 100 slots + 100 tags = 209; the root distance is 200, requiring one byte, giving 212. For a counterexample, make one tiny integer in [1,2] indirect: the extra scalar payload adds a byte without reducing the already one-byte vector stride.

20. A work item can carry slot, parent width, child width, type, depth, and a continuation state. A memo key for an indirect interpretation includes buffer identity, target, type, and child width. Store “active” separately from “complete.” Charge every visit and every newly scanned text extent against explicit budgets; reuse completed validation where the interpretation and immutable ownership match.

Solutions 21–25: engineering

21. Include signed extremes, unsigned extremes, direct and indirect floats, embedded-zero strings, empty blobs, every typed family, fixed tuples, different root/child widths, and different map key/value widths. A writer and reader that both reverse an offset base or both mistake width codes for actual widths can agree with each other while failing an independent reader.

22. A generic-cell scan touches a tag and payload per cell, typically sixteen bytes of cell storage before external payloads. A native typed array can resolve type and ownership once and read fixed-stride numbers. A verified typed wire vector can resolve type/width once, then use specialized endian-aware loads. Hoist container type, width dispatch, count, and stable bounds outside the element loop where the contract permits it.

23. Return full-range integers as BigInt, or use a checked exact-number conversion. Return blobs through a byte-oriented type. Return strings using an explicit byte length and a specified UTF-8 decoder; embedded zeros are not terminators in the logical string. A plain JSON round-trip is not a lossless bridge for every supported value and type distinction.

24. Immediate publication of every edit makes per-edit rebuilding a relevant baseline. An editor that applies a thousand changes before sending one snapshot should compare one freeze per batch, not a thousand freezes. Conversion overhead can dominate the first workload while native update savings dominate the second.

25. Add intern_key(text) -> Key and overload hot lookup to accept Key. The handle carries document identity and a stable key ID. Validate the identity at the boundary; do not accept an unqualified integer that could mean a different string in another document. Arbitrary-string lookup remains available for cold paths.

Solutions 26–32: the capstone

26. A read-mostly configuration may remain a verified wire buffer or use WireLog for occasional replacement snapshots. A rapidly growing working state is a stronger LiveFlex candidate. Large vector edits expose rebuild cost; frequent snapshots can erase native-editing gains. Shared-identity mutation and persistent path replacement have different alias behavior, so choose the semantics before comparing speed.

27. With capacity 1,250,000, generic cells need 20,000,000 payload bytes and a typed 64-bit lane needs 10,000,000. Each has a container descriptor, plus directory, allocator, root, and alignment overhead. The estimate excludes child allocations, snapshots, and unused allocator-retained blocks.

28. Growth changes the payload pointer/capacity and relevant layout version but not slot index or generation. Collection releases an unreachable payload and increments its generation before slot reuse. A span into the old payload is invalid after either relocation or reclamation. The logical handle survives only the first operation.

29. Establish scalar probe correctness against a standard map. In particular, a tombstone cannot terminate lookup, and insertion must search for an existing key before reusing a tombstone. Stage rehash into a new block and publish only after success. A layout epoch invalidates cached bucket indices after growth or rearrangement.

30. An old snapshot resolving through the current mutable directory would follow the new payload pointer and observe the update. Give the old and new snapshots distinct immutable directory views, sharing unchanged pages and payloads. The writer clones the affected directory page and payload; ownership retains the old version until its readers finish.

31. Every cached serialized ancestor that embeds the old child representation is invalid, including ancestors reachable through different aliases. An unchanged key set says only that map membership is stable, not that descendants are unchanged. Track content dependencies or rebuild/validate the relevant cache from descendant versions.

32. A valid gate names the behavior that must remain identical, tests adversarial and failure cases, measures the target workload and memory cost, and states a removal criterion. For example, retain SIMD probing only when it improves the intended map-size distribution enough to justify its extra platform code while matching the scalar path on the full stateful corpus.

REFERENCE & COMPLETE SOURCE


Book contents · Previous: Appendix A Type Codes and Byte Equations · Next: Appendix C Reading the Official Implementation