Clunkyard WebAssembly ABI 4
A WebAssembly robot runs inside Wasmtime under build/clunkyard-wasm-host,
which links the robot core (sdk/core) natively and speaks protocol 2 for
the robot. The robot calls the core through imports from the clunkyard
module, one for each API entry, and the core reads each hello and state and
writes the ready and each act. There is no WASI context and no other import.
abi4/clunkyard.h is the C SDK. It is generated from
protocol/v2/api.json (make api); do not edit it. It declares every
import at the name libclunkyard has, without the handle (an instance is
one robot), with the plain names (cy_sensors_nearest) aliasing the latest
version, and the records and constants the core uses.
#include "clunkyard.h"
static void turn(void) {
cy_contact target;
if (cy_sensors_nearest(&target) == CY_OK) {
cy_gun_aim_heading(target.bearing);
cy_gun_fire();
}
}
CY_ROBOT(NULL, turn)
The same surface, generated beside it, for the other languages:
| Language | File | Exports through |
|---|---|---|
| C | abi4/clunkyard.h |
CY_ROBOT(start, turn) |
| Rust | clunkyard.rs (mod clunkyard; in a #![no_std] crate, which it gives a trapping panic handler) |
cy_robot!(start, turn); |
| Zig | clunkyard.zig (@import("clunkyard.zig"); build with -rdynamic) |
comptime { cy.exportRobot(start, turn); } |
| AssemblyScript | clunkyard.ts (--runtime stub --use abort=clunkyard/cyAbort) |
cy.robot(start, turn) and export { clunkyard_start, clunkyard_turn } from "./clunkyard" |
| WAT | clunkyard.wat, pasted first in the module, before any other definition |
the module's own clunkyard_start and clunkyard_turn |
Each declares the imports at their C names in Rust (cy_sensors_nearest)
and at their import names elsewhere (sensors_nearest), with the records
(a view over the record's bytes in AssemblyScript, field offsets as globals
in WAT) and the constants. Only the C header has the DSL words import.
The module
A module exports:
memory
clunkyard_start() -> i32 once, after the hello; 0 to play on
clunkyard_turn() -> i32 once a state; 0 to send the act
CY_ROBOT(start, turn) writes both; start may be NULL. Its imports must
all be clunkyard.* functions with exactly the generated signatures. An
unknown name, a wrong signature or any other import fails at link, before
the hello: that is the version negotiation. A module without
clunkyard_start, such as one built for ABI 1 to 3, is refused.
Every import returns a status (CY_OK 0, CY_NONE 1 for absent, CY_SHORT
2 for a short buffer, negative for an error) and writes its result through
pointers into guest memory. The host bounds-checks every pointer and length
against memory as it is at the call, so a module may grow its memory.
cy_robot_seed(&seed) is the host's own import, not the core's: the seed
for the robot's own randomness, from CLUNKYARD_ROBOT_SEED, which the
referee derives from the match seed and the robot's id. It defaults to 1.
Metering
Fuel counts the guest's own operators. Each import also takes a nominal fee
from the turn's fuel, at once: 32 a call, whatever the core does for it, and
1 a byte for the data it moves (text in, a list's records, text out). The
fees are frozen per season in rates/s3.json (reflex.imports); make import-costs checks that they keep the core's real work inside the CPU
cap. An import that would overdraw takes the rest of the fuel, is not
answered, and the turn forfeits WASM_TURN_FUEL.
Work the core does natively sits outside fuel, so the host also holds a turn
to 256 import calls and 200 ms of its thread's CPU; over either, the turn
forfeits WASM_CPU. The season's limits are reflex.wasmFuelPerTurn and
reflex.wasmFuelPerMatch in rates/s3.json: a floor of the calls a turn
makes to act plus the thinking the interpreted DSL allows.
Building
scripts/build-wasm.sh
builds build/clunkyard-wasm-host and the example robot,
build/wasm-sentry-c.wasm, from bots/c/sentry.c.
It needs cc, Clang with the wasm32 target, the Wasmtime C API
(WASMTIME_PREFIX, default /usr/local) and the robot core: the image's
/opt/clunkyard/sdk/lib/libclunkyard.a, or cargo to build one from
sdk/core. A robot is freestanding C:
clang --target=wasm32 -mbulk-memory -std=c11 -O2 -nostdlib -Isdk/wasm/abi4 robot.c \
-Wl,--no-entry -Wl,--export-memory -o robot.wasm
With no libc, a struct copy or a zeroed array needs memcpy or memset;
-mbulk-memory lowers them to memory.copy and memory.fill, which the
host runs. Recent Clang turns it on by default; Clang 18, the image's,
does not.
The Rust starter in src/public/starters/rust-wasm uses the Rust surface. The DSL -wasm builds (scripts/build-dsl-wasm.sh) lower
Vane, Cadence and Glyph to the same ABI.
Running the host by hand
build/clunkyard-wasm-host robot.wasm \
--init-fuel 250000 --turn-fuel 5807 --match-fuel 10452600 \
--memory-bytes 4194304 --table-elements 256 --wasm-stack-bytes 262144 \
--cpu-ms 200 --import-calls 256 < protocol/v2/fixtures/2d-classic.ndjson
The environment sets the same limits (CLUNKYARD_WASM_INIT_FUEL,
CLUNKYARD_WASM_TURN_FUEL, CLUNKYARD_WASM_MATCH_FUEL,
CLUNKYARD_WASM_MEMORY_BYTES, CLUNKYARD_WASM_TABLE_ELEMENTS,
CLUNKYARD_WASM_STACK_BYTES); the command line takes precedence. Each act
carries the turn's fuel (wasmFuel), the match's (wasmFuelTotal), memory
and the import calls (wasmImports). A budget exhausted, a trap or memory
growth refused ends the robot with a structured failure.
Names
Every entry of the robot API 2.0.0 in WebAssembly (C). names.md has every language side by side.
| Canonical | Form | WebAssembly (C) |
|---|---|---|
match.mode |
property | cy_match_mode(&out) |
match.ruleset |
property | cy_match_ruleset(&out) |
match.control |
property | cy_match_control(&out) |
match.robots |
property | cy_match_robots(&out) |
match.is_3d |
property | cy_match_is_3d(&out) |
match.tick_rate |
property | cy_match_tick_rate(&out) |
match.max_ticks |
property | cy_match_max_ticks(&out) |
match.robot_id |
call | cy_robot_id(slot, &buf, &length) |
clock.tick |
property | cy_clock_tick(&out) |
clock.seconds |
property | cy_clock_seconds(&out) |
clock.remaining_ticks |
property | cy_clock_remaining_ticks(&out) |
arena.size |
property | cy_arena_size(&out) |
arena.gravity |
property | cy_arena_gravity(&out) |
arena.terrain_kind |
property | cy_arena_terrain_kind(&out) |
arena.ground_height |
call | cy_arena_ground_height(x, y, &out) |
me.health |
property | cy_me_health(&out) |
me.position |
property | cy_me_position(&out) |
me.velocity |
property | cy_me_velocity(&out) |
me.heading |
property | cy_me_heading(&out) |
me.pitch |
property | cy_me_pitch(&out) |
me.roll |
property | cy_me_roll(&out) |
me.angular_velocity |
property | cy_me_angular_velocity(&out) |
me.attitude |
property | cy_me_attitude(&out) |
me.grounded |
property | cy_me_grounded(&out) |
me.altitude |
property | cy_me_altitude(&out) |
me.applied |
property | cy_me_applied(&out) |
battery.charge |
property | cy_battery_charge(&out) |
battery.capacity |
property | cy_battery_capacity(&out) |
battery.regen |
property | cy_battery_regen(&out) |
battery.sustainable_drive |
property | cy_battery_sustainable_drive(&out) |
battery.compute |
property | cy_battery_compute(&out) |
battery.drive_cost |
call | cy_battery_drive_cost(fraction, &out) |
drive.hover_lift |
property | cy_drive_hover_lift(&out) |
drive.set |
control | cy_drive_throttle(value), cy_drive_turn(value), cy_drive_pitch_turn(value) |
drive.forces |
control | cy_drive_thrust(value), cy_drive_strafe(value), cy_drive_lift(value), cy_drive_yaw_torque(value), cy_drive_pitch_torque(value), cy_drive_roll_torque(value), cy_drive_brake(value) |
drive.get |
call | cy_drive_get_throttle(&out), cy_drive_get_turn(&out), cy_drive_get_pitch_turn(&out), cy_drive_get_thrust(&out), cy_drive_get_strafe(&out), cy_drive_get_lift(&out), cy_drive_get_yaw_torque(&out), cy_drive_get_pitch_torque(&out), cy_drive_get_roll_torque(&out), cy_drive_get_brake(&out) |
gun.aim_now |
property | cy_gun_aim_now(&out) |
gun.cooldown_ticks |
property | cy_gun_cooldown_ticks(&out) |
gun.spread |
property | cy_gun_spread(&out) |
gun.max_power |
property | cy_gun_max_power(&out) |
gun.damage_scale |
property | cy_gun_damage_scale(&out) |
gun.can_fire |
call | cy_gun_can_fire(power, reserve, &out) |
gun.shot_speed |
call | cy_gun_shot_speed(power, &out) |
gun.shot_cost |
call | cy_gun_shot_cost(power, &out) |
gun.reload_ticks |
call | cy_gun_reload_ticks(power, &out) |
gun.shot_damage |
call | cy_gun_shot_damage(power, &out) |
gun.power_for_speed |
call | cy_gun_power_for_speed(speed, &out) |
gun.power_for_flight_time |
call | cy_gun_power_for_flight_time(range, seconds, &out) |
gun.lead |
call | cy_gun_lead(contact, power, &out) |
gun.lead_point |
call | cy_gun_lead_point(position, velocity, power, &out) |
gun.aim |
control | cy_gun_aim_heading(value), cy_gun_aim_pitch(value) |
gun.fire |
control | cy_gun_fire(), cy_gun_power(value) |
gun.get |
call | cy_gun_get_aim_heading(&out), cy_gun_get_aim_pitch(&out), cy_gun_get_fire(&out), cy_gun_get_power(&out) |
scanner.last |
property | cy_scanner_last(&out) |
scanner.proximity_range |
property | cy_scanner_proximity_range(&out) |
scanner.reach |
call | cy_scanner_reach(arc, &out) |
scanner.arc_for_reach |
call | cy_scanner_arc_for_reach(range, &out) |
scanner.set |
control | cy_scanner_heading(value), cy_scanner_arc(value), cy_scanner_pitch(value) |
scanner.get |
call | cy_scanner_get_heading(&out), cy_scanner_get_arc(&out), cy_scanner_get_pitch(&out) |
sensors.contacts |
property | cy_sensors_contacts(&out, &count) |
sensors.nearest |
property | cy_sensors_nearest(&out) |
sensors.cues |
property | cy_sensors_cues(&out, &count) |
sensors.contact |
call | cy_sensors_contact(slot, &out) |
sensors.position_of |
call | cy_sensors_position_of(contact, &out) |
tracker.cue |
property | cy_tracker_cue(&out) |
tracker.nearest_bearing_rate |
property | cy_tracker_nearest_bearing_rate(&out) |
tracker.velocity |
call | cy_tracker_velocity(slot, &out) |
tracker.position |
call | cy_tracker_position(slot, &out) |
tracker.last_seen |
call | cy_tracker_last_seen(slot, &out) |
tracker.configure |
call | cy_tracker_configure(smoothing, forget_ticks, cue_ticks) |
nav.zone |
property | cy_nav_zone(&out) |
nav.zone.margin |
call | cy_nav_zone_margin(point, &out) |
nav.zone.contains |
call | cy_nav_zone_contains(point, margin, &out) |
nav.zone.distance_from |
call | cy_nav_zone_distance_from(point, &out) |
nav.storm |
property | cy_nav_storm(&out) |
nav.storm.margin |
call | cy_nav_storm_margin(point, &out) |
nav.storm.contains |
call | cy_nav_storm_contains(point, margin, &out) |
nav.storm.distance_from |
call | cy_nav_storm_distance_from(point, &out) |
nav.hill |
property | cy_nav_hill(&out) |
nav.hill.margin |
call | cy_nav_hill_margin(point, &out) |
nav.hill.contains |
call | cy_nav_hill_contains(point, margin, &out) |
nav.hill.distance_from |
call | cy_nav_hill_distance_from(point, &out) |
nav.pickups |
property | cy_nav_pickups(&out, &count) |
nav.nearest_pickup |
call | cy_nav_nearest_pickup(kind, &out) |
nav.field_distance |
call | cy_nav_field_distance(a, b, &out) |
score.me |
property | cy_score_me(&out) |
score.best |
property | cy_score_best(&out) |
score.armour |
property | cy_score_armour(&out) |
angles.normalize |
call | cy_angles_normalize(degrees, &out) |
angles.angle_diff |
call | cy_angles_angle_diff(target, current, &out) |
angles.bearing_to |
call | cy_angles_bearing_to(from, to, &out) |
angles.elevation_to |
call | cy_angles_elevation_to(from, to, &out) |
angles.direction |
call | cy_angles_direction(heading, pitch, &out) |
angles.distance |
call | cy_angles_distance(a, b, &out) |
angles.clamp |
call | cy_angles_clamp(value, low, high, &out) |
status.set |
control | cy_status_text(text), cy_status_value(value) |
status.get |
call | cy_status_get_text(&buf, &length), cy_status_get_value(&out) |
raw.number |
call | cy_raw_number(path, &out) |
raw.text |
call | cy_raw_text(path, &buf, &length) |
raw.has |
call | cy_raw_has(path, &out) |
raw.flag |
call | cy_raw_flag(path, &out) |
raw.count |
call | cy_raw_count(path, &out) |
raw.line |
call | cy_raw_line(&buf, &length) |
raw.extra |
call | cy_raw_extra(json) |
session.log |
call | cy_session_log(line) |