Skip to content

Program Structure

View as Markdown

Use this page when you need the smallest reliable mental model for how a Rust program starts, reads input, and exits.

#![no_std]
#![no_main]
use thru_sdk_macros::*;
#[entry(stack_size = 4096)]
fn main(_instr_data: &[u8]) -> Result<u64, u64> {
Ok(0)
}

Every program is #![no_std] and #![no_main]. thru-core installs the #[panic_handler], so do not define your own.

#[entry] comes from thru-sdk-macros. It generates the _start boot shim that the VM jumps to, sets up the stack, and exports the start symbol that calls your function.

#[entry(stack_size = 8192)]
ArgumentRules
stack_sizeStack size in bytes. Must be a multiple of 4096. Defaults to 4096 when omitted, but the argument list must not be empty.

The macro validates your function signature and fails to compile otherwise:

fn main(instr_data: &[u8]) -> Result<u64, u64>
fn main(instr_data: &[u8], mgr: AccountManager<N>) -> Result<u64, u64>
fn main(instr_data: &[u8]) -> !
fn main(instr_data: &[u8], mgr: AccountManager<N>) -> !

When the second parameter is present, the macro builds the AccountManager from the current transaction for you and exits with code 1 if it cannot be constructed.

StepWhat happens
EnterThe VM jumps to the generated _start, which validates the transaction version, sizes the stack segment, and calls start.
Read inputsstart turns the instruction-data pointer and length into a &[u8] (empty when the pointer is null) and optionally constructs the AccountManager.
RunYour entry function runs with that slice.
ExitA returned Ok(code) exits with revert = 0; Err(code) exits with revert = 1. A !-returning entry must exit itself.
use thru_core::program_utils;
program_utils::succeed(0); /* exit, no revert */
program_utils::revert(7); /* exit with revert */

Both wrap the exit syscall and never return. See Error Handling and Return Codes.

Programs with several instructions usually tag-dispatch on the first byte of the instruction data:

#![no_std]
#![no_main]
use thru_core::*;
use thru_sdk_macros::*;
#[entry(stack_size = 8192)]
fn main(instr_data: &[u8], mgr: AccountManager<4>) -> Result<u64, u64> {
let Some((tag, rest)) = instr_data.split_first() else {
return Err(1);
};
match tag {
0 => initialize(rest, &mgr),
1 => transfer(rest, &mgr),
_ => Err(2),
}
}
use thru_core::tvm_println;
tvm_println!("balance is {}", balance);
tvm_println!(bufsize = 128, "short message {}", idx);

tvm_println! formats into a stack-allocated heapless::String (1024 bytes by default) and passes it to the log syscall.