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)

Download this page as Markdown