Lua and WebAssembly #
JavaScript is the shortest path into Budo, but it is not the only one. Lua apps use the same sys vocabulary with Lua-friendly tables. WebAssembly apps import host functions from env and exchange strings and data through linear memory.
This page focuses on what changes when you leave JavaScript.
Lua lifecycle #
Create main.lua in the project directory. Budo loads it once, then your code registers an animation callback.
-- Store the circle position between frames.
local x = 80
-- Budo calls this function when a frame is scheduled.
local function frame(timestamp)
-- Clear the previous frame.
sys.canvas.clear('#101116')
-- Choose the circle fill color.
sys.canvas.setFillColor('#7c6ff7')
-- Draw the circle at the current x position.
sys.canvas.drawCircle(x, 100, 32)
-- Move the circle a little for the next frame.
x = x + 1
-- Request another frame.
sys.animation.requestFrame(frame)
end
-- Start the animation loop.
sys.animation.requestFrame(frame)sys.animation.start(callback) is available as an alias for requestFrame.
Lua exposes the standard libraries and adds sys, console, fetch, and json_parse.
Lua data shapes #
Lua arrays are 1-based. APIs that return lists follow that convention.
-- Read the available MIDI input devices.
local devices = sys.midi.getInputDevices()
-- Lua arrays returned by Budo are 1-based.
for i = 1, #devices do
-- Log the device name for a picker or diagnostics.
console.log(devices[i].name)
endBinary reads return Lua strings rather than JavaScript ArrayBuffer objects.
-- Read binary data as a Lua string.
local data = sys.file.readBinary('image.bin')
-- Use the string length operator to count bytes.
console.log('Bytes: ' .. #data)SQLite queries return arrays of row tables.
-- Open or create the SQLite database.
local db = sys.db.open('notes')
-- Query rows as an array of Lua tables.
local rows = sys.db.query(db, 'SELECT id, title FROM notes')
-- Iterate through the returned rows.
for i = 1, #rows do
-- Read a column by name.
console.log(rows[i].title)
endLua neural tensors use a table with data, optional shape, and returned dtype.
-- Run inference with a named input tensor.
local result = sys.neural.run(model, {
-- The table key must match the model input name.
input = {
-- Tensor data is supplied as a Lua array.
data = {0.0, 1.0, 0.5, 0.25},
-- Shape follows the tensor dimensions expected by the model.
shape = {1, 4}
}
})Lua fetch #
Lua fetch is callback-based.
-- Fetch data from an allowed domain.
fetch('https://api.example.com/data', function(response, error)
-- Handle policy or transport errors first.
if error then
-- Log the error text supplied by the runtime.
console.log(error)
return
end
-- Parse the JSON response body into a Lua table.
local data = json_parse(response.body)
-- Log the HTTP status code.
console.log('Status: ' .. response.status)
end)The response table includes status, statusText, ok, url, redirected, headers, body, and bodyLen.
Lua differences from JavaScript #
The largest difference is not naming; it is runtime idiom. Lua uses tables, callbacks, and explicit 1-based arrays. JavaScript uses promises, typed arrays, and modules. Budo keeps method names close enough that you can translate examples mentally.
There is no sys.timer in Lua. Use the frame timestamp or write a tiny scheduler in Lua when you need delayed actions.
There is no Budo-specific Lua module resolver. Keep simple apps in one file or use standard Lua loading patterns available in the runtime.
WebAssembly lifecycle #
Desktop WebAssembly apps may be written as WAT for small examples or compiled from another language into .wasm. Budo looks for optional init and frame exports.
(module
;; Import a host function that clears the canvas with an ARGB color.
(import "env" "canvas_clear" (func $canvas_clear (param i32)))
;; Import a host function that draws a circle.
(import "env" "canvas_draw_circle" (func $canvas_draw_circle (param f32 f32 f32)))
;; Import a host function that sets the fill color.
(import "env" "canvas_set_fill_color" (func $canvas_set_fill_color (param i32)))
;; Export a frame function so Budo can call it each frame.
(func (export "frame") (param $timestamp f32)
;; Clear the frame with an opaque dark color.
i32.const 0xff101116
call $canvas_clear
;; Select an opaque purple fill color.
i32.const 0xff7c6ff7
call $canvas_set_fill_color
;; Set the circle center x coordinate.
f32.const 120
;; Set the circle center y coordinate.
f32.const 100
;; Set the circle radius.
f32.const 32
;; Draw the circle with the current paint.
call $canvas_draw_circle
)
)frame receives a timestamp in milliseconds. If no frame export exists, no per-frame guest callback runs. If init exists, Budo calls it once after instantiation.
WebAssembly memory and strings #
Numeric-only imports do not need guest memory. String-backed APIs need an exported memory so Budo can read UTF-8 bytes from the module.
(module
;; Export memory so the host can read string bytes.
(memory (export "memory") 1)
;; Store a UTF-8 string at byte offset 16.
(data (i32.const 16) "Hello from WASM")
;; Import a text drawing function that reads pointer and length pairs.
(import "env" "canvas_draw_text" (func $canvas_draw_text (param i32 i32 f32 f32 f32) (result f32)))
;; Export the frame function called by Budo.
(func (export "frame") (param f32)
;; Pass the string pointer.
i32.const 16
;; Pass the string length.
i32.const 15
;; Pass the x position.
f32.const 40
;; Pass the y position.
f32.const 80
;; Pass the font size.
f32.const 24
;; Draw the text and receive its measured width.
call $canvas_draw_text
;; Drop the measured width because this example does not need it.
drop
)
)The same pointer-and-length pattern appears in file, network, shader, SVG, font, SQLite, MIDI, and neural host imports.
WebAssembly capabilities #
Some imports exist only when a capability is available. Advanced Skia paint imports are a good example. A module that imports a missing host function will fail to instantiate, so either target a known platform or split optional features behind capability-specific modules.
Capability host imports return integer availability flags such as capability_neural, capability_udp, and capability_shaders_advanced.
Choosing a runtime #
JavaScript and TypeScript are best for fast iteration, modules, typed arrays, and broad examples. Lua is excellent for compact scripts, embeddable game logic, and teams that prefer a small dynamic language. WebAssembly is useful when you already have a compiled core, want a stable ABI boundary, or need to share logic with another platform.
The drawing and platform model is the same across all three. Choose the guest language for your app architecture, not because the runtime pushes you toward one.