Native value lifetimes
jank is native Clojure (C++/LLVM) — no JVM, no Java interop, no REPL. The compiler statically type-checks the boundary between jank objects and native C++ values, and this page is about the single rule that boundary enforces: a native cpp value only stays native within the form that produced it. Every lesson below is a consequence of that rule. Each lesson names the committed example that proves it — those files are the running test suite for this document.
The one rule that explains most crashes
A native cpp value only stays native within the form that produced it. A Color, Vector2, Rectangle, Camera2D, ... may be:
- constructed inline as a call argument —
(cpp/DrawCircleV (cpp/Vector2 ...) ...)✅ - bound to a
let-local and used in the same scope ✅ - bound in a
letOUTSIDE the frame loop and used inside it (lexical capture) ✅ — this is how create-once GPU resources live (lines_drawing.jank's RenderTexture,words_alignment.jank's Font)
But it may NOT cross a jank fn boundary:
- returned from a fn ❌ —
returning a native object of type 'Color', which is not convertible to a jank runtime object. Even via nestedif(dashed_line.janklearned this). - passed as a fn parameter and then used in a native call ❌ — it boxes to an
object_refand the native call rejects it (digital_clock.jank's draw-hand;tiled_drawing.jankhit this trying to pass aTexture2Dto adraw-tiledhelper —No matching call to 'DrawTexturePro' ... argument 0 having type 'jank::runtime::object_ref &'— and had to inline the helper so the texture stayed a capturedlet-local). - carried through
loop/recurstate ❌ (input_mouse.jank).
Fix: thread plain jank data (ints, reals, keywords, maps) and resolve the native value inline at the use site. camera_2d_platformer.jank threads the camera as five scalars and rebuilds (cpp/Camera2D ...) each frame; input_mouse.jank threads a color-id int and picks the Color with a nested if at draw time.
Frame-crossing mutable native state: park it in a cpp/raw static
When a native resource must BOTH persist across frames AND be recreated at runtime with computed sizes, neither of the two usual homes works: loop/ recur state can't carry a native value (the same rule as above — a native value can't cross a fn boundary — applied to loop/recur state), and a create-once outer-let local can't be rebound. Park the value in a cpp/raw static with tiny accessor fns instead:
(cpp/raw "static RenderTexture2D jank_target = { 0 };
static void jank_resize_target(int w, int h) {
UnloadRenderTexture(jank_target);
jank_target = LoadRenderTexture(w, h);
}
static RenderTexture2D jank_get_target(void) { return jank_target; }")
jank calls (cpp/jank_resize_target w h) on change events and re-fetches (let [target (cpp/jank_get_target)] ...) each frame -- the struct comes back by value into a let-local and never crosses a jank fn boundary. Proof: viewport_scaling.jank, whose RenderTexture is recreated on every window resize / resolution / viewport-mode change with sizes computed from the current window state. (UnloadRenderTexture guards id 0 internally, so the first call against the zero-initialized static is safe.) Reach for this only when recreation is genuinely dynamic; a fixed-size resource should stay a create-once outer-let local (lines_drawing.jank).
CRITICAL: cpp/raw statics are duplicated PER JANK FN. Every jank fn that references the shims gets its OWN copy of the raw block's statics – a helper fn that writes the "same" static writes a private copy the other fns never see. Probe evidence (2026-07-04): after -main called a load-into-static shim, -main read .glyphCount 95 from its copy while a helper defn- read 0 from its own; the helper's writes were likewise invisible to -main. Failure modes are nasty: state silently "resets" across fn boundaries, and reading through a zeroed struct's pointer field segfaults. Rule: route EVERY read/write of a mutable raw static through one single jank fn (in practice -main), inlining helper logic there -- unicode_ranges.jank inlines the C's AddCodepointRange into -main's rebuild block for exactly this reason. Pure-jank helpers (no shim calls) remain safe to factor out. (viewport_scaling.jank was unaffected only because all its shim calls already sat in -main; compute_hash.jank / storage_values.jank / codepoints_loading.jank are safe because their statics are written and read within one fn call's dynamic extent, not across fns.)
Create-once native resources
LoadRenderTexture / GetFontDefault style resources bind in a let outside the frame loop and get used inside it — lexical capture keeps them native. Unload after the loop.
(let [canvas (cpp/LoadRenderTexture WIDTH HEIGHT)]
(loop [...]
... (cpp/BeginTextureMode canvas) ...)
(cpp/UnloadRenderTexture canvas))
Two RenderTextures at once work (camera_2d_split_screen.jank). Blit a RenderTexture with a negative source height — RTs are stored upside down (lines_drawing.jank, window_letterbox.jank).