Skip to main content

Entrypoint

Every guest program must opt out of the standard Rust runtime and declare its entry point. Without it, the zkVM has no function to execute.

entrypoint!​

Registers a function as the zkVM entry point and opts the crate out of the standard Rust runtime. The #![no_main] crate attribute is also required — the zkVM has no OS to provide a default runtime or main symbol.

ziskos::entrypoint!(fn_name)

Parameters​

NameTypeDescription
fn_nameidentifierName of the function to register as the guest entry point. It must take no arguments, and its return type must implement ZiskTermination — (), i32, or Result<T, E>.

Example​

#![no_main]
ziskos::entrypoint!(main);

fn main() {
// guest code runs here
}
note

The #![no_main] attribute is required at the crate root. Without it, the Rust compiler generates a default main function that conflicts with the #[no_mangle] main the macro produces, causing a linker error.

The macro expands to a #[no_mangle] extern "C" fn main() -> i32 that initializes the guest runtime, calls the registered function, tears the runtime down, and returns the exit code its return value maps to.


ZiskTermination​

macro_rules! cannot branch on the signature of the function it is handed — macros expand before type checking — so entrypoint! resolves the entry point's return type through a trait instead, the same role std::process::Termination plays for a regular fn main.

pub trait ZiskTermination {
fn to_exit_code(self) -> i32;
}

Any return type implementing this trait is accepted by entrypoint!. Three implementations ship with ziskos:

Return typeExit code
()0 — a plain return is a successful completion.
i32The returned value, verbatim.
Result<T, E> where T: ZiskTerminationOk(value) delegates to value.to_exit_code(); Err(_) is 1.

Because Result delegates through its Ok type, Result<(), E> exits with 0 on success and Result<i32, E> exits with the returned code. E is unconstrained — it does not need to implement Debug or Display.

New in 1.1.0-alpha

Earlier versions accepted only fn() -> (). Existing entry points keep working unchanged: () maps to exit code 0, which is what the macro previously returned unconditionally.

Examples​

Returning an explicit code:

#![no_main]
ziskos::entrypoint!(main);

use ziskos::io;

fn main() -> i32 {
let n: u64 = io::read();
if n == 0 {
return 2; // reject the input without panicking
}
io::commit(&(n * 2));
0
}

Propagating errors with ?:

#![no_main]
ziskos::entrypoint!(main);

use ziskos::io;

fn main() -> Result<(), MyError> {
let raw = io::read_slice();
let parsed = parse(raw)?; // an Err here exits with code 1
io::commit(&parsed.digest);
Ok(())
}
warning

The Err value is discarded, not printed. ZiskTermination cannot format it without coupling the trait to a target-specific writer, so an error return produces exit code 1 and nothing else. Log the error inside the entry point before returning if you need a diagnostic:

fn main() -> Result<(), MyError> {
if let Err(e) = run() {
println!("guest failed: {e:?}");
return Err(e);
}
Ok(())
}

Where the exit code goes​

The code the macro computes is placed in a0 and handed to the termination mechanism of whichever target the program was built for:

TargetDestination
ZisK guestThe exit syscall (93), which ends the run.
QEMUThe sifive_test device: 0 becomes 0x5555 (pass), anything else (code << 16) | 0x3333.
NativeThe process exit status, as with any C main.
note

Under the zkVM the exit code terminates the run but is not currently reported back by the emulator, so a non-zero code does not by itself mark an execution or proof as failed. It is most useful when running the guest natively for debugging. Anything the verifier must observe has to be committed with io::commit.