|
| 1 | +--- |
| 2 | +title: Getting Started with Hyperlight |
| 3 | +description: A step-by-step guide to building your first Hyperlight host and guest on Linux |
| 4 | +--- |
| 5 | + |
| 6 | +This guide walks you through building a complete Hyperlight application from scratch on Linux. By the end, you'll have a **guest** program running inside a hardware-isolated micro-VM and a **host** program that creates the VM and calls a function inside it. |
| 7 | + |
| 8 | +## What you're building |
| 9 | + |
| 10 | +Hyperlight applications have two parts: |
| 11 | + |
| 12 | +- **Guest** — a small program that runs *inside* a micro-VM. It has no operating system, no file system access, and no network. It can only do what the host explicitly allows. |
| 13 | +- **Host** — a normal program that creates the micro-VM, loads the guest into it, and calls functions the guest exposes. |
| 14 | + |
| 15 | +In this guide, the guest will expose a `PrintOutput` function. The host will call it with a string, and the guest will ask the host to print it. |
| 16 | + |
| 17 | +## Prerequisites |
| 18 | + |
| 19 | +You need a Linux machine (or WSL2) with **KVM** enabled. Most modern Linux distributions ship with KVM support in the kernel. |
| 20 | + |
| 21 | +### 1. Check that KVM is available |
| 22 | + |
| 23 | +```sh |
| 24 | +ls -l /dev/kvm |
| 25 | +``` |
| 26 | + |
| 27 | +You should see output like `crw-rw---- 1 root kvm ...`. If the file doesn't exist, your CPU may not support hardware virtualization, or KVM modules aren't loaded. See the [Ubuntu KVM installation guide](https://help.ubuntu.com/community/KVM/Installation) for help. |
| 28 | + |
| 29 | +Make sure your user has permission to access `/dev/kvm`: |
| 30 | + |
| 31 | +```sh |
| 32 | +groups | grep kvm |
| 33 | +``` |
| 34 | + |
| 35 | +If `kvm` isn't listed, add yourself to the group: |
| 36 | + |
| 37 | +```sh |
| 38 | +sudo usermod -aG kvm $USER |
| 39 | +``` |
| 40 | + |
| 41 | +Then log out and log back in for the change to take effect. |
| 42 | + |
| 43 | +### 2. Install build tools |
| 44 | + |
| 45 | +On Ubuntu/Debian: |
| 46 | + |
| 47 | +```sh |
| 48 | +sudo apt update && sudo apt install -y build-essential |
| 49 | +``` |
| 50 | + |
| 51 | +On Azure Linux: |
| 52 | + |
| 53 | +```sh |
| 54 | +sudo dnf install -y build-essential |
| 55 | +``` |
| 56 | + |
| 57 | +### 3. Install Rust |
| 58 | + |
| 59 | +If you don't have Rust installed yet: |
| 60 | + |
| 61 | +```sh |
| 62 | +curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh |
| 63 | +``` |
| 64 | + |
| 65 | +Hyperlight requires Rust **1.89 or later**. After installing, verify your version: |
| 66 | + |
| 67 | +```sh |
| 68 | +rustc --version |
| 69 | +``` |
| 70 | + |
| 71 | +### 4. Install cargo-hyperlight |
| 72 | + |
| 73 | +`cargo-hyperlight` is a Cargo subcommand that handles cross-compiling guest binaries for the Hyperlight target. Install it with: |
| 74 | + |
| 75 | +```sh |
| 76 | +cargo install --locked cargo-hyperlight |
| 77 | +``` |
| 78 | + |
| 79 | +This gives you the `cargo hyperlight build` command, which takes care of all the special compilation settings that guests need — no extra configuration required. |
| 80 | + |
| 81 | +--- |
| 82 | + |
| 83 | +## Step 1: Create a workspace |
| 84 | + |
| 85 | +We'll use a Cargo workspace to keep the host and guest in one place. Create a project directory: |
| 86 | + |
| 87 | +```sh |
| 88 | +mkdir hyperlight-hello && cd hyperlight-hello |
| 89 | +``` |
| 90 | + |
| 91 | +Create a `Cargo.toml` at the root that declares both crates: |
| 92 | + |
| 93 | +```toml |
| 94 | +[workspace] |
| 95 | +members = ["guest", "host"] |
| 96 | +resolver = "2" |
| 97 | +``` |
| 98 | + |
| 99 | +--- |
| 100 | + |
| 101 | +## Step 2: Create the guest |
| 102 | + |
| 103 | +The guest is the program that will run inside the micro-VM. Generate the crate: |
| 104 | + |
| 105 | +```sh |
| 106 | +cargo new --name guest guest |
| 107 | +``` |
| 108 | + |
| 109 | +### 2a. Set up `guest/Cargo.toml` |
| 110 | + |
| 111 | +Replace the contents of `guest/Cargo.toml` with: |
| 112 | + |
| 113 | +```toml |
| 114 | +[package] |
| 115 | +name = "guest" |
| 116 | +version = "0.1.0" |
| 117 | +edition = "2024" |
| 118 | + |
| 119 | +[dependencies] |
| 120 | +hyperlight-common = { version = "0.11.0", default-features = false } |
| 121 | +hyperlight-guest = "0.11.0" |
| 122 | +hyperlight-guest-bin = "0.11.0" |
| 123 | +``` |
| 124 | + |
| 125 | +Here's what each dependency does: |
| 126 | + |
| 127 | +| Crate | Purpose | |
| 128 | +|---|---| |
| 129 | +| `hyperlight-common` | Shared types used by both host and guest (function call wrappers, parameter types, etc.) | |
| 130 | +| `hyperlight-guest` | Core guest library — provides the low-level VM exit mechanism and host communication | |
| 131 | +| `hyperlight-guest-bin` | Higher-level guest utilities — panic handler, heap initialization, host function helpers, and the `#[guest_function]` / `#[host_function]` macros | |
| 132 | + |
| 133 | +### 2b. Write the guest code |
| 134 | + |
| 135 | +Replace `guest/src/main.rs` with: |
| 136 | + |
| 137 | +```rust |
| 138 | +#![no_std] |
| 139 | +#![no_main] |
| 140 | +extern crate alloc; |
| 141 | + |
| 142 | +use alloc::string::String; |
| 143 | +use alloc::vec::Vec; |
| 144 | + |
| 145 | +use hyperlight_common::flatbuffer_wrappers::function_call::FunctionCall; |
| 146 | +use hyperlight_common::flatbuffer_wrappers::guest_error::ErrorCode; |
| 147 | + |
| 148 | +use hyperlight_guest::bail; |
| 149 | +use hyperlight_guest::error::Result; |
| 150 | +use hyperlight_guest_bin::{guest_function, host_function}; |
| 151 | + |
| 152 | +// Declare a host function that the guest can call. |
| 153 | +// The host must register a function with this exact name ("HostPrint") |
| 154 | +// before the guest tries to use it. |
| 155 | +#[host_function("HostPrint")] |
| 156 | +fn host_print(message: String) -> Result<i32>; |
| 157 | + |
| 158 | +// Define a guest function that the host can call. |
| 159 | +// When the host calls "PrintOutput", this function runs inside the VM. |
| 160 | +#[guest_function("PrintOutput")] |
| 161 | +fn print_output(message: String) -> Result<i32> { |
| 162 | + // Call the host function to print the message. |
| 163 | + // The guest can't print directly — it has no OS or stdout. |
| 164 | + // Instead, it asks the host to do it. |
| 165 | + let result = host_print(message)?; |
| 166 | + Ok(result) |
| 167 | +} |
| 168 | + |
| 169 | +/// Called once when the guest starts up. |
| 170 | +/// Use this for any initialization your guest needs. |
| 171 | +#[no_mangle] |
| 172 | +pub extern "C" fn hyperlight_main() { |
| 173 | + // Nothing to initialize for this simple example. |
| 174 | +} |
| 175 | + |
| 176 | +/// The dispatch function routes incoming function calls from the host. |
| 177 | +/// If the host calls a function name the guest doesn't recognize, this |
| 178 | +/// returns an error. |
| 179 | +#[no_mangle] |
| 180 | +pub fn guest_dispatch_function(function_call: FunctionCall) -> Result<Vec<u8>> { |
| 181 | + let function_name = function_call.function_name; |
| 182 | + bail!(ErrorCode::GuestFunctionNotFound => "{function_name}"); |
| 183 | +} |
| 184 | +``` |
| 185 | + |
| 186 | +Let's break down what's happening: |
| 187 | + |
| 188 | +- **`#![no_std]` and `#![no_main]`** — The guest runs inside a bare-metal micro-VM with no operating system. There's no `std` library and no normal `main()` entry point. |
| 189 | +- **`extern crate alloc`** — Even without `std`, Hyperlight provides a heap allocator so you can use `String`, `Vec`, and other heap types. |
| 190 | +- **`#[host_function("HostPrint")]`** — This macro declares that a function called `HostPrint` exists on the host side. When the guest calls `host_print(...)`, it triggers a VM exit, the host executes the function, and the result comes back. |
| 191 | +- **`#[guest_function("PrintOutput")]`** — This macro registers a function that the host can call into the guest. The host will call `"PrintOutput"` by name. |
| 192 | +- **`hyperlight_main()`** — This is the guest's entry point, called once when the VM starts. You can register functions or perform setup here. |
| 193 | +- **`guest_dispatch_function()`** — This handles any function calls that aren't covered by the `#[guest_function]` macro. In this example, it just returns an error for unknown functions. |
| 194 | + |
| 195 | +### 2c. Build the guest |
| 196 | + |
| 197 | +```sh |
| 198 | +cargo hyperlight build --release -p guest |
| 199 | +``` |
| 200 | + |
| 201 | +This cross-compiles the guest for the `x86_64-hyperlight-none` target. The resulting binary will be at: |
| 202 | + |
| 203 | +``` |
| 204 | +target/x86_64-hyperlight-none/release/guest |
| 205 | +``` |
| 206 | + |
| 207 | +This binary is a tiny, self-contained program — no OS, no kernel, just your code and the Hyperlight guest runtime. |
| 208 | + |
| 209 | +--- |
| 210 | + |
| 211 | +## Step 3: Create the host |
| 212 | + |
| 213 | +The host is a normal Linux program. It creates a micro-VM, loads the guest binary into it, and calls guest functions. Generate the crate: |
| 214 | + |
| 215 | +```sh |
| 216 | +cargo new --name host host |
| 217 | +``` |
| 218 | + |
| 219 | +### 3a. Set up `host/Cargo.toml` |
| 220 | + |
| 221 | +Replace the contents of `host/Cargo.toml` with: |
| 222 | + |
| 223 | +```toml |
| 224 | +[package] |
| 225 | +name = "host" |
| 226 | +version = "0.1.0" |
| 227 | +edition = "2024" |
| 228 | + |
| 229 | +[dependencies] |
| 230 | +hyperlight-host = "0.11.0" |
| 231 | +anyhow = "1.0" |
| 232 | +``` |
| 233 | + |
| 234 | +| Crate | Purpose | |
| 235 | +|---|---| |
| 236 | +| `hyperlight-host` | The Hyperlight host library — creates and manages micro-VMs, calls guest functions | |
| 237 | +| `anyhow` | Convenient error handling (optional, but makes the example cleaner) | |
| 238 | + |
| 239 | +### 3b. Write the host code |
| 240 | + |
| 241 | +Replace `host/src/main.rs` with: |
| 242 | + |
| 243 | +```rust |
| 244 | +use anyhow::Context as _; |
| 245 | +use hyperlight_host::GuestBinary; |
| 246 | +use hyperlight_host::sandbox::SandboxConfiguration; |
| 247 | + |
| 248 | +fn main() -> anyhow::Result<()> { |
| 249 | + // Path to the guest binary we built in Step 2. |
| 250 | + let guest_path = std::env::args() |
| 251 | + .nth(1) |
| 252 | + .context("Usage: host <path-to-guest-binary>")?; |
| 253 | + |
| 254 | + // Tell Hyperlight where to find the guest binary. |
| 255 | + let guest = GuestBinary::FilePath(guest_path); |
| 256 | + |
| 257 | + // Configure the micro-VM's memory. |
| 258 | + // The guest runs in a fixed-size memory region — there's no virtual memory |
| 259 | + // or swap. 1 MiB each for heap and stack is plenty for this example. |
| 260 | + let mut config = SandboxConfiguration::default(); |
| 261 | + config.set_heap_size(1024 * 1024); // 1 MiB heap |
| 262 | + config.set_stack_size(1024 * 1024); // 1 MiB stack |
| 263 | + |
| 264 | + // Create the sandbox (micro-VM). |
| 265 | + // `new()` sets up the VM and loads the guest binary. |
| 266 | + // `evolve()` initializes it — this calls `hyperlight_main()` in the guest, |
| 267 | + // which registers the guest's functions and makes them callable. |
| 268 | + let mut sandbox = hyperlight_host::UninitializedSandbox::new( |
| 269 | + guest, |
| 270 | + Some(config), |
| 271 | + )?.evolve()?; |
| 272 | + |
| 273 | + // Call the guest's "PrintOutput" function. |
| 274 | + // This triggers the VM to start executing guest code. |
| 275 | + // Inside the guest, `print_output()` will call the host's "HostPrint" |
| 276 | + // function, which prints the message to stdout. |
| 277 | + let bytes_printed: i32 = sandbox.call( |
| 278 | + "PrintOutput", |
| 279 | + "Hello from Hyperlight! I am running inside a micro-VM.\n".to_string(), |
| 280 | + )?; |
| 281 | + |
| 282 | + println!("Guest function returned: {bytes_printed} bytes printed"); |
| 283 | + |
| 284 | + Ok(()) |
| 285 | +} |
| 286 | +``` |
| 287 | + |
| 288 | +Here's the flow, step by step: |
| 289 | + |
| 290 | +1. **Load the guest binary** — `GuestBinary::FilePath(...)` tells Hyperlight to read the compiled guest from disk. |
| 291 | +2. **Configure memory** — `SandboxConfiguration` controls how much heap and stack memory the guest gets. The guest can't grow beyond this. |
| 292 | +3. **Create the sandbox** — `UninitializedSandbox::new(...)` creates the micro-VM using KVM. This sets up the hardware isolation — the guest gets its own memory region that the host CPU enforces. |
| 293 | +4. **Evolve** — `.evolve()` transitions the sandbox from "uninitialized" to "ready." During this step, Hyperlight runs `hyperlight_main()` in the guest, which registers guest functions. |
| 294 | +5. **Call a guest function** — `sandbox.call("PrintOutput", ...)` tells the VM to execute the guest's `PrintOutput` function. The guest runs, calls back to the host to print, and returns the result. |
| 295 | + |
| 296 | +### 3c. Build the host |
| 297 | + |
| 298 | +```sh |
| 299 | +cargo build --release -p host |
| 300 | +``` |
| 301 | + |
| 302 | +--- |
| 303 | + |
| 304 | +## Step 4: Run it |
| 305 | + |
| 306 | +```sh |
| 307 | +./target/release/host ./target/x86_64-hyperlight-none/release/guest |
| 308 | +``` |
| 309 | + |
| 310 | +You should see: |
| 311 | + |
| 312 | +```text |
| 313 | +Hello from Hyperlight! I am running inside a micro-VM. |
| 314 | +Guest function returned: 57 bytes printed |
| 315 | +``` |
| 316 | + |
| 317 | +That message was printed by the **host**, but the request to print it came from the **guest** running inside a hardware-isolated micro-VM. The guest called `host_print(...)`, which triggered a VM exit, the host executed the print, and the result was sent back to the guest — all in under a millisecond. |
| 318 | + |
| 319 | +--- |
| 320 | + |
| 321 | +## What just happened? |
| 322 | + |
| 323 | +Here's the full sequence of events: |
| 324 | + |
| 325 | +``` |
| 326 | +Host Guest (inside micro-VM) |
| 327 | +───────────────────────────── ───────────────────────────── |
| 328 | +1. Creates micro-VM via KVM |
| 329 | +2. Loads guest binary into VM |
| 330 | +3. Calls evolve() |
| 331 | + 4. hyperlight_main() runs |
| 332 | + (registers PrintOutput) |
| 333 | +5. Calls "PrintOutput" |
| 334 | + 6. print_output() executes |
| 335 | + 7. Calls host_print() → VM exit |
| 336 | +8. Host prints the message |
| 337 | +9. Returns result to guest |
| 338 | + 10. Guest receives result |
| 339 | + 11. Returns to host |
| 340 | +12. Receives return value |
| 341 | +``` |
| 342 | + |
| 343 | +The key insight is that the guest **never has direct access to the host's memory, file system, or network**. Every interaction goes through explicitly registered functions — the host controls exactly what the guest can do. |
| 344 | + |
| 345 | +--- |
| 346 | + |
| 347 | +## Next steps |
| 348 | + |
| 349 | +- **Add more guest functions** — Use `#[guest_function("Name")]` to expose additional functions, and call them from the host with `sandbox.call("Name", ...)`. |
| 350 | +- **Register host functions** — Before calling `.evolve()`, use `sandbox.register("FunctionName", |args| { ... })` to make host-side functions available to the guest. |
| 351 | +- **Explore Hyperlight Wasm** — If you want to run WebAssembly inside a micro-VM, check out [hyperlight-wasm](https://github.com/hyperlight-dev/hyperlight-wasm). |
| 352 | +- **Browse more examples** — The [cargo-hyperlight examples](https://github.com/hyperlight-dev/cargo-hyperlight/tree/main/examples) show additional patterns including C FFI in guests. |
| 353 | +- **Join the community** — Chat with the team on the [CNCF Slack #hyperlight channel](https://cloud-native.slack.com/archives/hyperlight). |
0 commit comments