tbcx — serialize, load, and inspect precompiled Tcl 9.1 bytecode (procs, OO methods, and lambdas). Artifacts require an exact Tcl major/minor match at load time.
tbcx::save in out ?-include-source?
tbcx::load in
tbcx::dump filename
tbcx::gc
The tbcx extension provides four commands that enable an efficient save → load → eval pipeline for Tcl 9.1 scripts.
The goal is to pay the cost of parsing/compiling at save time so that loading is as fast as reading a compact binary, while remaining functionally equivalent to source of the original script.
Artifacts store the compiled top-level, precompiled proc bodies, TclOO method/ctor/dtor bodies, and lambda literals for use with apply. Loading installs these into the current interpreter and executes the top-level block in the caller’s current namespace, with source-equivalent semantics for variable scope, frame identity, and info script.
In safe interpreters, tbcx_SafeInit provides the package and type infrastructure but does not register any tbcx::* commands. A parent interpreter may selectively grant access with interp alias or interp expose.
Synopsis — Compile a script and write a .tbcx artifact.
in — Resolved in this order:
out — One of:
-translation binary -eofchar {}) is enforced. The caller’s channel settings are mutated and not restored. The channel is not closed.-include-source — Optional flag. Embeds the authored source text of every proc and TclOO method body in the artifact as an LPString. Required when consumers depend on info body, info class definition, TIP #280 line attribution in stack traces, disassembly annotations, or introspection-based clone idioms (e.g. cloneRule, installTocRule) to return the original authored text. Artifact size grows proportional to the aggregate source text of all procs and methods.
Default behavior (no -include-source): Every proc/method body source field is emitted as an empty LPString. At load time the loader substitutes the diagnostic sentinel described in SOURCE PRESERVATION below, so info body returns a loud string instead of silently returning empty. This matches the 25-year tclcompiler/tbcload precedent for Tcl AOT compile output: deployment artifacts are typically distributed instead of source, and stripping keeps the artifact smaller and preserves the IP-protection angle.
Examples
# Save from path → path (default: source stripped)
set path [tbcx::save ./app.tcl ./app.tbcx]
# Save from path → path with body source preserved for
# info body / info class definition / TIP #280 line attribution.
tbcx::save ./app.tcl ./app.tbcx -include-source
# Save from string value → path
set script {proc hi {} {puts Hello}; hi}
tbcx::save $script ./hello.tbcx
# Save from channel → channel
set in [open ./lib/foo.tcl r]
set out [open ./foo.tbcx w]
fconfigure $out -translation binary -eofchar {}
try {
tbcx::save $in $out
} finally {
close $in
close $out
}
Synopsis — Load a .tbcx artifact, install precompiled entities, and execute the top-level block in the caller’s current namespace.
in — One of:
.tbcx stream (binary)..tbcx file.uplevel 1 [list tbcx::load $path] to pop to the outer frame — identical to the pattern already required for source in the same position..tcl source path when the artifact was built from a file, matching what source would have set. This supports both [file dirname [info script]] (for sibling asset lookup) and [info script] eq $::argv0 (self-invocation guards).Examples
# Load into current interp
tbcx::load ./app.tbcx
# Load from an open channel
set ch [open ./app.tbcx r]
fconfigure $ch -translation binary -eofchar {}
try {
tbcx::load $ch
} finally {
close $ch
}
# Drop-in replacement for source, inside a module loader proc.
# Both branches use uplevel 1 so they reach the caller’s frame.
proc moduleLoad {path} {
set tbcxPath [file join [file dirname $path] ../tbcx [file tail $path].tbcx]
if {[file exists $tbcxPath]} {
uplevel 1 [list tbcx::load $tbcxPath]
} else {
uplevel 1 [list source $path]
}
}
Synopsis — Disassemble and describe a .tbcx artifact in human-readable form.
.tbcx file.source: <stripped at save time>.Examples
% package require tbcx
% tbcx::save hello.tcl hello.tbcx -include-source
% puts [tbcx::dump hello.tbcx]
TBCX Header:
magic = 0x58434254 ('T''B''C''X')
format = 92
tcl_version = 9.1.0 (type 0)
top: code=12, except=0, lits=2, aux=0, locals=1, stack=2
source = /path/to/hello.tcl
Top-level block:
Disassembly (top-level):
...
Procs: 1
- proc hi (ns=::)
args:
source: (14 bytes)
puts Hello
Disassembly (hi):
...
Synopsis — Purge stale entries from the per-interpreter lambda shimmer-recovery registry.
tbcx::gc is a no-op if no ApplyShim has been installed yet (i.e. before any tbcx::load call), and it is safe to call multiple times.
Without -include-source, every proc and method body is emitted with an empty source-text field on the wire, and the loader installs a two-line diagnostic sentinel as the body’s string representation:
# tbcx: body source stripped at save time; info body unavailable
error "tbcx: introspection-based cloning is not supported for this artifact"
The shape matches the canonical tclcompiler pattern (Compiler8.html, “Example 1: Cloning Procedures”):
proc new [info args orig] [info body orig] and invokes new,
the clone fails loudly rather than silently running as a no-op.With -include-source, the authored source text is preserved in the artifact and attached as the body Tcl_Obj’s string representation via Tcl_InvalidateStringRep + Tcl_InitStringRep. The ByteCode internal representation is untouched; execution still uses the precompiled bytecode. info body, info class definition, info class constructor, and TIP #280 source attribution round-trip byte-for-byte.
This section summarizes the on-disk structure. Format version is 92 (Tcl 9.1). All integers are little-endian.
0x58434254) + format version (92) + producing Tcl version;
size/count metadata for the top-level block (code length, exception ranges,
literal count, AuxData count, locals, max stack); authored source path LPString
(empty for inline/channel inputs).
Literals in the script that represent lambdas for apply (lists of the form
{args body ?ns?}) are compiled and serialized as lambda-bytecode literals at
save time. On load, the compiled body, argument list, and optional namespace element are
rehydrated into a Proc and registered in the ApplyShim so that the first
call to apply does not trigger compilation. If type shimmer later evicts the
lambdaExpr internal representation, the ApplyShim transparently re-installs it on the
next apply call.
Artifacts are portable across little- and big-endian hosts. The loader detects host byte order once and applies a tight in-place swap over bulk sections that require conversion, avoiding per-field branching so cross-endian loads remain close to native performance.
Loading evaluates the precompiled top-level in the caller’s current namespace with iPtr->scriptFile set to the authored source path (if recorded), then installs precompiled proc/method bodies and rehydrates lambda literals. The intent is to be functionally indistinguishable from source of the original script, with the benefit of faster startup due to avoided parsing/compilation.
TBCX precompiles bodies and lambdas only when they are present in statically identifiable literal positions (script-body arguments to commands like foreach, while, try, eval, etc., or lambda literals for apply). Strings assembled at runtime — for example with format, string interpolation, or list construction — still round-trip correctly, but they remain ordinary data and compile at execution time when Tcl evaluates them.
TBCX preserves normal TclOO class/object construction semantics by executing the rewritten top-level script, while substituting precompiled bodies for recognized oo::define / oo::objdefine method forms. Tested scenarios include class methods, self methods, per-object methods, private methods, inheritance (including diamond), mixins, filters, forwards, abstract/singleton metaclasses, method rename/delete/export changes, metaclasses with self method, and next-based constructor chaining.
With -include-source, info class definition, info class constructor,
info class destructor, and info object method all return the authored body
text byte-for-byte, enabling introspection-based idioms such as cloneMethod src dst
patterns to work identically to the source-based baseline.
TBCX follows Tcl’s standard threading model: only the thread that created an interpreter may call tbcx::save, tbcx::load, tbcx::dump, or tbcx::gc on that interpreter. Multi-thread support means multiple independent interpreters, each used by its owning thread — not sharing one interpreter across threads. Calling a TBCX command from a non-owning thread returns TCL_ERROR with a diagnostic message.
Artifacts are designed to load into interpreters other than the originating one. Interpreter-specific state (ApplyShim lambda registry, load depth, OO shim hidden-ID counter) remains strictly per-interpreter and is cleaned up automatically when the interpreter is deleted.
Sanity caps exist for code size (64 MiB), literal/AuxData/exception counts (1M each), string lengths (4 MiB), total serialized output (256 MB), serialization recursion depth (64), total WriteLiteral calls (2M), and total WriteCompiledBlock calls (256K). Exceeding any limit produces an error.
Nested or reentrant tbcx::load calls are capped at depth 8 per interpreter to prevent runaway recursive loading.
Representative messages include: “bad header”, “incompatible Tcl version”, “short read/write”, “unsupported AuxData kind”, “input is neither an open channel nor a readable file”, “runaway serialization detected”, “tbcx::save: unknown option …; expected -include-source”, and Tcl errors from top-level evaluation.
Loading executes code. Only load artifacts you trust. Safe interpreters receive no tbcx::* commands by default; use interp alias or interp expose from a parent interpreter to grant selective access.
source(n), TclOO(n), info(n), tclcompiler (historical Tcl 8.x AOT tool)
© 2025–2026 Miguel Banon
MIT License.