Lua-like API (avm.h)
Lua-like API (avm.h)
avm.h provides a high-level embedding API modelled after the Lua C API.
If you have ever embedded Lua in a C application, the pattern will feel
immediately familiar — create a state, register host functions, load code,
call it, read results, close the state.
#include "avm.h"
Design philosophy
| Concept | Lua C API | armvm avm API |
|---|---|---|
| VM state handle | lua_State *L |
avm_State *L |
| Host function type | lua_CFunction |
avm_CFunction |
| Create state | luaL_newstate() |
avm_newstate() |
| Destroy state | lua_close() |
avm_close() |
| Register host function | lua_register() |
avm_register() |
| Load source code | luaL_loadbuffer() |
avm_loadbuffer() |
| Execute code | lua_call() |
avm_call() |
| Read integer result | lua_tointeger() |
avm_tointeger() |
| Push integer return | lua_pushinteger() |
avm_pushinteger() |
In Lua, values pass through a virtual stack. In armvm, arguments and return values pass through ARM registers (r0–r15) following the standard ARM calling convention. The accessor functions use 1-based indices (index 1 = r0, index 2 = r1, …) to match Lua’s 1-based stack convention.
Types
avm_State
typedef struct VM avm_State;
The opaque VM state. All avm_* functions take a pointer to this type as
their first argument. Obtain one from avm_newstate() and release it with
avm_close().
Notable public fields (read-only after avm_loadbuffer):
| Field | Type | Description |
|---|---|---|
r[0..15] |
DWORD[16] |
ARM registers; r0=result after avm_call |
memory |
BYTE * |
Base of VM-addressable memory |
progsize |
DWORD |
Size of loaded bytecode in bytes |
stacksize |
DWORD |
Stack region size |
heapsize |
DWORD |
Heap region size |
entry_point |
DWORD |
Byte offset of _main; set by avm_loadbuffer |
avm_CFunction
typedef int (*avm_CFunction)(avm_State *L);
Prototype for every host function registered with avm_register().
- Arguments: read from registers using
avm_tointeger(L, 1)(= r0),avm_tointeger(L, 2)(= r1), etc. - Return values: write with
avm_pushinteger(L, result)to set r0. - Return the count: return 0 if void (no result), 1 after pushing one result. Only r0 is used for return values; returning a count higher than 1 has no additional effect.
#include <string.h>
#include "avm.h"
/* Example: strlen wrapper */
static int host_strlen(avm_State *L) {
const char *s = avm_tostring(L, 1); /* r0 = VM-relative pointer */
avm_pushinteger(L, (int)strlen(s)); /* result → r0 */
return 1; /* one return value */
}
State management
avm_newstate
avm_State *avm_newstate(DWORD stack_size, DWORD heap_size);
Allocates and initialises a new VM state. No bytecode is loaded yet — call
avm_register() for every host function you need, then call
avm_loadbuffer().
| Parameter | Description |
|---|---|
stack_size |
Stack region size in bytes (VM_STACK_SIZE = 64 KB by default) |
heap_size |
Heap region size in bytes (VM_HEAP_SIZE = 64 KB by default) |
Returns a non-NULL avm_State * on success.
avm_close
void avm_close(avm_State *L);
Frees all memory associated with the state. Do not use L afterwards.
Registering host functions
avm_register
void avm_register(avm_State *L, const char *name, avm_CFunction fn);
Registers a C function so that ARM assembly can call it with
bl _<name>.
| Parameter | Description |
|---|---|
L |
VM state |
name |
Symbol name without a leading underscore |
fn |
C function to call at runtime |
Must be called before avm_loadbuffer() because the assembler resolves
bl _name references at compile time. Calling avm_register after
avm_loadbuffer has no effect on already-compiled code.
avm_register(L, "strlen", host_strlen);
avm_register(L, "malloc", host_malloc);
avm_register(L, "puts", host_puts);
ARM assembly side:
bl _strlen @ calls host_strlen(L); r0 = strlen(r0)
bl _malloc @ calls host_malloc(L); r0 = address of allocated block
Internals: avm_register writes name into the global symbols[] array
at the next available index and stores fn in L->cfuncs[index]. The
internal _avm_dispatch syscall handler looks up L->cfuncs[call_id] and
calls it.
Note:
symbols[]is a global table shared across all states in the same process. Creating twoavm_Stateinstances and registering different functions will overwrite the same indices. Only one state (or one shared global registry) should be used for compilation at a time.
Loading and compiling code
avm_loadbuffer
int avm_loadbuffer(avm_State *L, const char *code, size_t len);
Compiles ARM assembly source text and loads the resulting bytecode into L.
| Parameter | Description |
|---|---|
L |
VM state |
code |
NUL-terminated ARM assembly source |
len |
Length of code in bytes (for API parity; compile_buffer reads to NUL) |
Returns 0 on success, −1 on compilation failure.
On success:
L->memorypoints to a freshly-allocated block containing the bytecode, stack space, and heap.L->progsizeis set to the number of bytecode bytes.L->entry_pointis set to the byte offset of the_mainlabel, or 0 if no_mainwas found.- The stack pointer (
L->r[SP_REG]) is reset toprogsize + stacksize.
avm_loadbuffer can be called multiple times on the same state — each call
replaces the previous bytecode. Host functions registered with
avm_register persist across calls.
Compilation internals:
- Resets the global compiler state (
cs): all label, symbol, and set tables are cleared. - Calls
compile_buffer, which performs a two-pass assembly:- Pass 1 encodes instructions and records forward-reference placeholders.
- Pass 2 (
linkprogram) patches the placeholders with resolved offsets.
- Reads the assembled bytes from the temporary file into a fresh memory
region of size
progsize + stacksize + heapsize. - Initialises the heap allocator at offset
progsize + stacksize.
Executing code
avm_call
void avm_call(avm_State *L, DWORD pc);
Executes the loaded bytecode starting at byte offset pc.
| Parameter | Description |
|---|---|
L |
VM state (must have had avm_loadbuffer called first) |
pc |
Entry point; use L->entry_point to start from _main |
avm_call is a thin wrapper around execute(). Execution terminates when
vm->location >= vm->progsize — normally when the top-level function
executes bx lr (because lr was initialised to vm->progsize).
After avm_call returns, read the result with:
int result = avm_tointeger(L, 1); /* register r0 */
Reading register values (avm_to*)
These functions read ARM register values from the state. Index 1 = r0,
index 2 = r1, …, index 16 = r15. In an avm_CFunction, arguments are in
the registers according to the ARM calling convention:
| Register | Argument / role |
|---|---|
| r0 (idx 1) | 1st argument; integer return value |
| r1 (idx 2) | 2nd argument |
| r2 (idx 3) | 3rd argument |
| r3 (idx 4) | 4th argument |
avm_tointeger
int avm_tointeger(avm_State *L, int idx);
Returns r[idx-1] reinterpreted as a signed 32-bit integer.
avm_touinteger
unsigned int avm_touinteger(avm_State *L, int idx);
Returns r[idx-1] as an unsigned 32-bit integer.
avm_tonumber
float avm_tonumber(avm_State *L, int idx);
Returns r[idx-1] reinterpreted as a 32-bit IEEE 754 float.
avm_tostring
const char *avm_tostring(avm_State *L, int idx);
Treats r[idx-1] as a byte offset into L->memory and returns a pointer to
that location. Use this when the ARM code passes a VM-relative string pointer
in a register.
static int host_puts(avm_State *L) {
puts(avm_tostring(L, 1)); /* r0 = offset of string in VM memory */
return 0;
}
Safety:
avm_tostringdoes not validate the offset. Add a bounds check if the offset comes from untrusted ARM code.
avm_topointer
void *avm_topointer(avm_State *L, int idx);
Like avm_tostring, but returns void * — useful when passing buffers.
avm_toboolean
int avm_toboolean(avm_State *L, int idx);
Returns 1 if r[idx-1] is non-zero, 0 otherwise.
Writing return values (avm_push*)
Inside an avm_CFunction, call these to set the return value that the ARM
caller will see in r0 after the bl _name returns.
avm_pushinteger
void avm_pushinteger(avm_State *L, int n);
Sets r0 = (DWORD)n.
avm_pushnumber
void avm_pushnumber(avm_State *L, float n);
Sets r0 to the bit pattern of the IEEE 754 float n.
avm_pushboolean
void avm_pushboolean(avm_State *L, int b);
Sets r0 = 1 if b is non-zero, r0 = 0 otherwise.
Complete example
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "avm.h"
/* ---- Host functions ---- */
static int host_strlen(avm_State *L) {
avm_pushinteger(L, (int)strlen(avm_tostring(L, 1)));
return 1;
}
static int host_puts(avm_State *L) {
puts(avm_tostring(L, 1));
return 0;
}
/* ---- ARM assembly source ---- */
static const char *src =
".globl _main\n"
"_main:\n"
" bl _puts\n" /* r0 must already point at a string */
" mov r0, #42\n"
" bx lr\n";
int main(void) {
/* 1. Create state */
avm_State *L = avm_newstate(VM_STACK_SIZE, VM_HEAP_SIZE);
/* 2. Register host functions BEFORE avm_loadbuffer */
avm_register(L, "strlen", host_strlen);
avm_register(L, "puts", host_puts);
/* 3. Compile and load */
if (avm_loadbuffer(L, src, strlen(src)) != 0) {
fprintf(stderr, "compilation failed\n");
avm_close(L);
return 1;
}
/* 4. Execute from _main */
avm_call(L, L->entry_point);
/* 5. Read result from r0 */
printf("returned %d\n", avm_tointeger(L, 1));
/* 6. Clean up */
avm_close(L);
return 0;
}
Reloading code
You can call avm_loadbuffer multiple times to replace the loaded program
without recreating the state or re-registering host functions:
avm_State *L = avm_newstate(VM_STACK_SIZE, VM_HEAP_SIZE);
avm_register(L, "puts", host_puts);
avm_loadbuffer(L, program_a, strlen(program_a));
avm_call(L, L->entry_point);
avm_loadbuffer(L, program_b, strlen(program_b)); /* replaces program_a */
avm_call(L, L->entry_point);
avm_close(L);
Each call to avm_loadbuffer resets the compiler state (labels, forward
references) and allocates a fresh memory region for the new bytecode.
Relationship to the low-level API
avm_* is built on top of the lower-level vm_create / execute interface.
You can mix the two in the same binary — for instance, use vm_create for
the execution side while still using avm_* accessors for reading/writing
registers. The state types are the same (avm_State * ≡ LPVM).
See API Reference for the low-level interface documentation.
Porting from the low-level API
| Old pattern | New equivalent |
|---|---|
memset(symbols, 0, …); strcpy(symbols[N], "name"); |
avm_register(L, "name", fn) |
Manually writing a VM_SysCall switch statement |
Not needed — avm_register installs a per-function dispatcher |
FILE *fp = tmpfile(); compile_buffer(…); fread(prog,…); vm_create(…) |
avm_loadbuffer(L, src, len) |
execute(vm, (DWORD)main_label) |
avm_call(L, L->entry_point) |
vm->r[0] |
avm_tointeger(L, 1) |
(char *)vm->memory + vm->r[0] |
avm_tostring(L, 1) |
vm_shutdown(vm) |
avm_close(L) |