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
| Name | Type | Description |
|---|---|---|
fn_name | identifier | Name 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
}
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 type | Exit code |
|---|---|
() | 0 — a plain return is a successful completion. |
i32 | The returned value, verbatim. |
Result<T, E> where T: ZiskTermination | Ok(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.
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(())
}
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:
| Target | Destination |
|---|---|
| ZisK guest | The exit syscall (93), which ends the run. |
| QEMU | The sifive_test device: 0 becomes 0x5555 (pass), anything else (code << 16) | 0x3333. |
| Native | The process exit status, as with any C main. |
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.