TRA and native values · dive 02
The shared ABI and bundled Lua
One getter, exported at load, hands other modules a versioned C table, and the table hands out three
Lua sources compiled into the library. Putting a source into the server stays an explicit
SCRIPT LOAD or FUNCTION LOAD by an authorized caller.
A Valkey module can share functions with other modules loaded into the same server: it exports a
pointer under a name, and another module requests that name from the server. trafikey exports
one getter. The getter returns a read-only table after a version and size check, and the table’s two
selectors return the bytes of the bundled Lua by id: a capabilities wrapper, a bounded bitmap count and a Function
library that holds both. The module never loads those bytes into the server’s script cache or its function
libraries. Negotiate a table and call a selector; then run the bounded count on three strings, through a loaded
script, a cold cache and a loaded library.
One getter, exported at load
The last step of init gives the server one function pointer under one name,
trafikey_get_api_v1. The server keeps it in its shared-API registry. Another module,
loaded after trafikey, requests the name with GetSharedAPI,
receives the pointer and calls it. That is the module’s one entry in the registry.
The repository carries such a consumer as a test fixture: a second module, traapitest,
written in C against the public header. At load it negotiates the table, checks the refusals, and registers
TRA.TESTSOURCE, which returns the provider’s bytes for source 0, 1 or 2 so a
binary-safe client can compare them with the three files. The runtime Dockerfile leaves it out, and the deploy
README calls it test-only and not a production API.
// trafikey — init, its last step: the getter exported under its name fn init(ctx: &Context, _: &[ValkeyString]) -> Status { // … the earlier steps of init registry::export_shared_api( ctx, c"trafikey_get_api_v1", tra::trafikey_get_api_v1 as *const std::ffi::c_void, ); Status::Ok }
One name in the server’s registry is the whole native surface another module can reach.
A table negotiated by version and size
The C header is the contract a consumer compiles against. It names the table, the getter’s type, four feature bits and three source ids, and it states the rules: negotiate the version, the size and the feature bits first; the bytes are length-delimited, immutable, owned by the module and not necessarily NUL-terminated; never free them or keep them past the provider’s lifetime. It calls itself an unreleased ABI 1 candidate.
// trafikey — include/trafikey.h, the public header, from its first comment /* Unreleased ABI 1 candidate. GetSharedAPI(ctx, "trafikey_get_api_v1") returns * TrafikeyGetApiV1. Negotiate version, struct size and feature bits first. * Script bytes are length-delimited, immutable, module-owned and not necessarily * NUL-terminated. Never free them or retain them beyond provider lifetime. * The original prefix through lua_source is unchanged; catalog is appended. * No dataset access or authorization bypass is exposed by this table. */ typedef struct TrafikeyApiV1 { uint32_t abi_version; uint32_t struct_size; uint64_t feature_bits; const uint8_t *(*lua_source)(size_t *length); const uint8_t *(*lua_source_by_id)(uint32_t id, size_t *length); } TrafikeyApiV1; typedef const TrafikeyApiV1 *(*TrafikeyGetApiV1)(uint32_t version, uint32_t minimum_size); #define TRAFIKEY_FEATURE_LUA_CAPABILITIES UINT64_C(1) #define TRAFIKEY_FEATURE_LUA_BITMAP_COUNT UINT64_C(2) #define TRAFIKEY_FEATURE_LUA_FUNCTIONS UINT64_C(4) #define TRAFIKEY_FEATURE_LUA_CATALOG UINT64_C(8) #define TRAFIKEY_LUA_CAPABILITIES UINT32_C(0) #define TRAFIKEY_LUA_BITMAP_COUNT UINT32_C(1) #define TRAFIKEY_LUA_FUNCTIONS UINT32_C(2) /* Request this prefix size only when using the old lua_source field. */ #define TRAFIKEY_API_V1_PREFIX_SIZE ((uint32_t)offsetof(TrafikeyApiV1, lua_source_by_id))
The Rust side mirrors the struct field for field under
#[repr(C)] and fills one static instance: an ABI version of 1, its own size, and feature bits
15, all four. The getter returns that instance only when the version is 1 and the size the caller needs fits
inside it; any other request returns NULL. On a 64-bit target the table is 32 bytes.
// trafikey — tra::TrafikeyApiV1, the table it shares and the getter it exports #[repr(C)] pub struct TrafikeyApiV1 { pub abi_version: u32, pub struct_size: u32, pub feature_bits: u64, pub lua_source: unsafe extern "C" fn(*mut usize) -> *const u8, pub lua_source_by_id: unsafe extern "C" fn(u32, *mut usize) -> *const u8, } // … static SHARED: TrafikeyApiV1 = TrafikeyApiV1 { abi_version: 1, struct_size: std::mem::size_of::<TrafikeyApiV1>() as u32, feature_bits: 15, lua_source, lua_source_by_id, }; #[no_mangle] pub extern "C" fn trafikey_get_api_v1(version: u32, minimum_size: u32) -> *const TrafikeyApiV1 { if version != 1 || minimum_size > SHARED.struct_size { return std::ptr::null(); } &SHARED }
The table keeps an original prefix. Its first selector,
lua_source, takes only a length pointer and returns source 0.
lua_source_by_id was appended after it and takes an id. The header defines
TRAFIKEY_API_V1_PREFIX_SIZE, the offset of the appended field, 24 bytes on a 64-bit
target, for a consumer that uses the old field alone; a consumer that calls the selector by id negotiates the
full size. Both selectors return NULL for a NULL length pointer. An unknown id writes length 0 through the
pointer and returns NULL. A known id writes the byte length and returns bytes the module keeps.
// trafikey — the three sources compiled in, and the selector that hands them out static LUA: &[u8] = include_bytes!("../../../lua/capabilities.lua"); static BITMAP_LUA: &[u8] = include_bytes!("../../../lua/bitmap_count.lua"); static FUNCTIONS_LUA: &[u8] = include_bytes!("../../../lua/functions.lua"); // … unsafe extern "C" fn lua_source_by_id(id: u32, length: *mut usize) -> *const u8 { if length.is_null() { return std::ptr::null(); } let bytes = match id { 0 => LUA, 1 => BITMAP_LUA, 2 => FUNCTIONS_LUA, _ => { // an unknown id: length 0 through the caller's pointer, and NULL unsafe { *length = 0; } return std::ptr::null(); } }; // a known id: its byte length through the caller's pointer, then the module-owned bytes unsafe { *length = bytes.len(); } bytes.as_ptr() }
Negotiate, then ask · pick a request and a call
negotiated: 32 bytes, all five fields
lua_source_by_id(1, &n) → a pointer to 1225 bytes of bitmap_count.lua (source 1), and n = 1225
the bytes: immutable, module-owned, not necessarily NUL-terminated; never freed, never kept past the provider
Version, then size, then id: every request the table cannot answer is NULL, and every answer it gives is bytes the module keeps.
Three sources
Source 0, capabilities.lua, is a read-only script, flagged
no-writes in its shebang line. It refuses any key or argument and returns what
TRA.CAPABILITIES returns, so it depends on the module being loaded.
-- trafikey bundled source 0, capabilities.lua — the whole file #!lua flags=no-writes -- Native module metadata only; no automatic installation or hidden keys. if #KEYS ~= 0 or #ARGV ~= 0 then return redis.error_reply('ERR TRA arguments: no keys or arguments expected') end return redis.call('TRA.CAPABILITIES')
Source 1, bitmap_count.lua, counts the set bits in a bounded
byte window of one string key. It takes exactly one key and two arguments, a start byte and a byte count. Each
argument is a canonical decimal of one to nine digits with no leading zero, and the parser checks the length
before it runs a pattern, so a long argument is refused without being matched. The count runs from 1 to 4096,
and the window ends inside the first 536,870,912 bytes, the same 232-bit domain as the module’s
offsets. The script calls the server’s own STRLEN and
BITCOUNT, with an explicit BYTE unit, and returns three
values: a version tag, the string’s length and the count. It calls no TRA
command.
-- trafikey bundled source 1, bitmap_count.lua — the whole file #!lua flags=no-writes -- Existing ordinary bitmap-string slice; no custom type, hidden keys or writes. local function bitmap_count(keys, args) if #keys ~= 1 or #args ~= 2 then return redis.error_reply('ERR TRA.LUA expected one key, start_byte and byte_count') end local function decimal(s) -- Bound parser work before pattern matching, including invalid input. if type(s) ~= 'string' or #s < 1 or #s > 9 then return nil end if not string.match(s, '^[0-9]+$') then return nil end if #s > 1 and string.sub(s, 1, 1) == '0' then return nil end return tonumber(s) end local start = decimal(args[1]) local count = decimal(args[2]) -- API work bound; not a Valkey memory/thread/latency tuning recommendation. if not start or not count or count < 1 or count > 4096 or start > 536870911 or start + count > 536870912 then return redis.error_reply('ERR TRA.LUA invalid or excessive byte range') end local length = redis.call('STRLEN', keys[1]) local cardinality = redis.call('BITCOUNT', keys[1], start, start + count - 1, 'BYTE') return {'trafikey.bitmap_count.v1', length, cardinality} end return bitmap_count(KEYS, ARGV)
Source 2, functions.lua, is a Function library named
trafikey. It carries the count function, the same code as source 1, and registers two functions,
trafikey_capabilities and trafikey_bitmap_count_v1, both
no-writes, under a comment that keeps installation explicit and never replaces an
existing library. None of the three sources reads or writes an emq: key.
-- trafikey bundled source 2, functions.lua — the library header and its two registrations #!lua name=trafikey -- Explicit installation only; never automatically replace an existing library. -- … the bitmap_count function: the same code as source 1's redis.register_function{ function_name = 'trafikey_capabilities', callback = function(keys, args) if #keys ~= 0 or #args ~= 0 then return redis.error_reply('ERR TRA arguments: no keys or arguments expected') end return redis.call('TRA.CAPABILITIES') end, flags = {'no-writes'} } redis.register_function{ function_name = 'trafikey_bitmap_count_v1', callback = bitmap_count, flags = {'no-writes'} }
The bounded count · pick a route, a key and the arguments
route: the script cache holds source 1 under its SHA
arguments: one key, two canonical decimals · start 0, count 2 → bytes 0..1
host calls: STRLEN bitmap → 2 · BITCOUNT bitmap 0 1 BYTE → 2
reply: ["trafikey.bitmap_count.v1", 2, 2]
writes: none: the source is no-writes and the call read-only
The bundled count is the server’s own STRLEN and
BITCOUNT behind a strict, bounded argument check, on any route that runs it.
Installing is an authorized step
Handing out bytes is not installing them. The deploy README names two routes, both explicit and both run by a
caller with the authority to change the server’s programmability state, even though the query itself
writes no key. The script route loads the exact reviewed bytes of the count with
SCRIPT LOAD and calls the returned SHA with the read-only
EVALSHA_RO. The function route loads the library with
FUNCTION LOAD and calls it with FCALL_RO.
# deploy — README, the two explicit routes (angle brackets are placeholders)
SCRIPT LOAD <exact reviewed bytes of lua/bitmap_count.lua>
EVALSHA_RO <returned SHA> 1 <authorized-existing-string-key> 0 2
=> ["trafikey.bitmap_count.v1", <logical length>, <exact count in bytes 0 and 1>]
FUNCTION LOAD <exact reviewed bytes of lua/functions.lua>
FCALL_RO trafikey_bitmap_count_v1 1 <authorized-existing-string-key> 0 2
The script cache is not deployment state. After a restart, or on another node, the
same EVALSHA_RO runs against a cold cache and gets a NOSCRIPT
error; the README’s rule is to reload only the exact trusted source and retry, and never to flush scripts
that belong to someone else. The function route offers no default REPLACE and no
FUNCTION FLUSH: a collision with an existing library is checked before loading. A
library whose capability function calls TRA.CAPABILITIES depends on the module, and
the README requires that dependency in the rollback record.
The repository checks the Lua at source level with a test double, lua-contracts.lua,
which runs both the script and the library function against a fake redis table: six
good cases, sixteen malformed argument lists, each refused with no host call, and the wrong key counts. It
prints its own limit with the pass line: not real Valkey, ACL or ABI integration.
The module ships bytes; an authorized caller decides whether they become a script or a library, and reloads them by hand when the cache is cold.
Pattern & implementation
Where Redis Patterns Applied loads a script once and calls it by its digest, this dive reads a module that ships its scripts as bytes and leaves the loading to an authorized caller.
Load the script, keep its SHA, call EVALSHA, and on
NOSCRIPT load it again and retry.
EVALSHA and NOSCRIPT teaches
it.
Three sources compiled into the library and handed to other modules by id through a negotiated C table; the
read-only count loaded by an authorized SCRIPT LOAD or
FUNCTION LOAD and called with EVALSHA_RO or
FCALL_RO.
References
Sources
- Valkey — Modules API reference — ExportSharedAPI and GetSharedAPI, the registry the getter lives in.
- Valkey — Programmability — scripts and functions, and the
no-writesflag. - Valkey — SCRIPT LOAD — the script route’s install step.
- Valkey — EVALSHA_RO — the read-only call by digest, and NOSCRIPT on a cold cache.
- Valkey — FUNCTION LOAD — the function route’s install step.
- Valkey — FCALL_RO — the read-only call into a library.
Related in this course
- TRA and native values — the module this dive belongs to.
- Bitmaps on strings — the previous dive: the same strings, counted natively.
- Loading the module — init, whose last step exports the getter.
- Commands, not scripts — the command form beside the script form.
- The native value type — the next dive.
- redis-patterns · EVALSHA and NOSCRIPT — the near side of the door.