Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@ jobs:
- name: Run example
shell: bash
run: just run-guest
- name: Test scaffold
Comment thread
jprendes marked this conversation as resolved.
Outdated
shell: bash
run: just test-new

spelling:
name: Spell check with typos
Expand Down
9 changes: 8 additions & 1 deletion justfile
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,15 @@ fmt:
cargo +nightly fmt --all -- --check
cargo +nightly fmt --all --manifest-path ./examples/host/Cargo.toml -- --check
cargo +nightly fmt --all --manifest-path ./examples/guest/Cargo.toml -- --check
# These are standalone template files not part of any crate, so cargo fmt wont find them.
rustfmt +nightly --check ./src/new/guest/_main.rs ./src/new/host/_main.rs

fmt-apply:
cargo +nightly fmt --all
cargo +nightly fmt --all --manifest-path ./examples/host/Cargo.toml
cargo +nightly fmt --all --manifest-path ./examples/guest/Cargo.toml
# These are standalone template files not part of any crate, so cargo fmt wont find them.
rustfmt +nightly ./src/new/guest/_main.rs ./src/new/host/_main.rs

clippy:
cargo clippy --all -- -D warnings
Expand All @@ -22,4 +26,7 @@ build-guest:
cargo hyperlight build --manifest-path ./examples/guest/Cargo.toml

run-guest: build-guest
cargo run --manifest-path ./examples/host/Cargo.toml -- ./target/x86_64-hyperlight-none/debug/guest
cargo run --manifest-path ./examples/host/Cargo.toml -- ./target/x86_64-hyperlight-none/debug/guest

test-new:
cargo test --test new
7 changes: 7 additions & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use std::env;

use cargo_hyperlight::cargo;

mod new;
mod perf;

const VERSION: &str = env!("CARGO_PKG_VERSION");
Expand All @@ -26,6 +27,12 @@ fn main() {
std::process::exit(1);
}
}
Some(a) if a == "new" => {
if let Err(e) = new::run(args) {
eprintln!("{e:?}");
std::process::exit(1);
}
}
_ => {
cargo()
.expect("Failed to create cargo command")
Expand Down
1 change: 1 addition & 0 deletions src/new/_gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
target
7 changes: 7 additions & 0 deletions src/new/guest/_Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
[package]
name = "{name}"
version = "0.1.0"
edition = "2024"

[dependencies]
hyperlight-guest-bin = "{version}"
40 changes: 40 additions & 0 deletions src/new/guest/_main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
#![no_std]
#![no_main]
extern crate alloc;

use alloc::string::String;
use core::sync::atomic::{AtomicI32, Ordering};

use hyperlight_guest_bin::error::Result;
use hyperlight_guest_bin::{guest_function, host_function};

static COUNTER: AtomicI32 = AtomicI32::new(0);

// Declare a host function that the guest can call. The string is the
// registration name (must match what the host passes to register()).
// If omitted, the Rust function name is used.
// The host must register this before the sandbox is initialized.
#[host_function("GetWeekday")]
fn get_weekday() -> Result<String>;

// Register a guest function that can be called by the host.
#[guest_function("SayHello")]
fn say_hello(name: String) -> Result<String> {
let weekday = get_weekday()?;
Ok(alloc::format!("Hello, {name}! Today is {weekday}."))
}

// Guest functions can take multiple arguments of different types.
#[guest_function("Add")]
fn add(a: i32, b: i32) -> Result<i32> {
Ok(a + b)
}

// Increments a counter and returns the new value. State persists across
// calls until the host restores a snapshot, which resets all VM memory
// back to the state it was in when the snapshot was taken.
#[guest_function("Increment")]
fn increment() -> Result<i32> {
let old = COUNTER.fetch_add(1, Ordering::Relaxed);
Ok(old + 1)
}
7 changes: 7 additions & 0 deletions src/new/host/_Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
[package]
name = "{name}"
version = "0.1.0"
edition = "2024"

[dependencies]
hyperlight-host = "{version}"
49 changes: 49 additions & 0 deletions src/new/host/_main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
use hyperlight_host::{GuestBinary, MultiUseSandbox, UninitializedSandbox};

fn main() -> hyperlight_host::Result<()> {
// Create a sandbox from the guest binary. It starts uninitialized so you
// can register host functions before the guest begins executing.
let mut sandbox = UninitializedSandbox::new(
GuestBinary::FilePath("../guest/target/{arch}-hyperlight-none/debug/{guest_name}".into()),
None,
)?;

// Register a host function that the guest can call.
sandbox.register("GetWeekday", weekday)?;

// Evolve into a MultiUseSandbox, which lets you call guest functions
// multiple times.
let mut sandbox: MultiUseSandbox = sandbox.evolve()?;

// Call a guest function with a single argument.
let result: String = sandbox.call("SayHello", "World".to_string())?;
println!("{result}");

// Multiple arguments are passed as a tuple.
let sum: i32 = sandbox.call("Add", (2_i32, 3_i32))?;
println!("2 + 3 = {sum}");

// Guest state persists between calls. Take a snapshot so we can
// restore back to this point later.
let snapshot = sandbox.snapshot()?;

let count: i32 = sandbox.call("Increment", ())?;
println!("count = {count}"); // 1
let count: i32 = sandbox.call("Increment", ())?;
println!("count = {count}"); // 2
let count: i32 = sandbox.call("Increment", ())?;
println!("count = {count}"); // 3

// Restore resets all guest memory back to the snapshot.
sandbox.restore(snapshot)?;

let count: i32 = sandbox.call("Increment", ())?;
println!("count after restore = {count}"); // 1 again

Ok(())
}

fn weekday() -> hyperlight_host::Result<String> {
// It's always Monday here, sorry Garfield!
Ok("Monday".to_string())
}
154 changes: 154 additions & 0 deletions src/new/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
use std::ffi::OsString;
use std::fs;
use std::path::{Path, PathBuf};

use anyhow::{Context, Result, ensure};
use clap::Parser;

const HYPERLIGHT_VERSION: &str = "0.15";
// TODO: support aarch64-hyperlight-none when aarch64 guests are supported.
const GUEST_ARCH: &str = "x86_64";

const GUEST_CARGO_TOML: &str = include_str!("guest/_Cargo.toml");
const GUEST_MAIN_RS: &str = include_str!("guest/_main.rs");
const HOST_CARGO_TOML: &str = include_str!("host/_Cargo.toml");
const HOST_MAIN_RS: &str = include_str!("host/_main.rs");
const GITIGNORE: &str = include_str!("_gitignore");

/// Create a new Hyperlight project.
#[derive(Parser, Debug)]
#[command(name = "new")]
struct NewCli {
/// Path to create the project at. The directory name is used as the crate
/// name (like `cargo new`).
path: PathBuf,

/// Skip generating the host crate.
#[arg(long, default_value_t = false)]
no_host: bool,

/// Skip generating the guest crate.
#[arg(long, default_value_t = false, conflicts_with = "no_host")]
no_guest: bool,
}

pub fn run(args: impl Iterator<Item = OsString>) -> Result<()> {
let cli = NewCli::parse_from(args);

let name = cli
.path
.file_name()
.context("Invalid project path")?
.to_str()
.context("Project name must be valid UTF-8")?;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

might want to do a bit more validation here on the name? I am not sure what makes up a valid project name but we would want to strip strings from beginning end etc. Maybe special characters?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done


validate_name(name)?;
ensure!(
!cli.path.exists(),
"Directory '{}' already exists",
cli.path.display()
);

match (cli.no_host, cli.no_guest) {
(true, false) => {
write_guest(&cli.path, name)?;
}
(false, true) => {
write_host(&cli.path, name, &format!("{name}-guest"))?;
}
(false, false) => {
let guest_name = format!("{name}-guest");
write_guest(&cli.path.join("guest"), &guest_name)?;
write_host(&cli.path.join("host"), &format!("{name}-host"), &guest_name)?;
}
(true, true) => unreachable!("clap rejects --no-host and --no-guest together"),
}
write_file(cli.path.join(".gitignore"), GITIGNORE)?;

let dir = cli.path.display();
println!("Created project at '{dir}'\n");
match (cli.no_host, cli.no_guest) {
(true, false) => {
println!("Build:");
println!(" cd {dir} && cargo hyperlight build");
}
(false, true) => {
println!("Build:");
println!(" cd {dir} && cargo build");
}
(false, false) => {
println!("Build and run:");
println!(" cd {dir}/guest && cargo hyperlight build");
println!(" cd {dir}/host && cargo run");
}
(true, true) => unreachable!(),
}

Ok(())
}

fn write_guest(dir: &Path, name: &str) -> Result<()> {
let cargo_toml = GUEST_CARGO_TOML
.replace("{name}", name)
.replace("{version}", HYPERLIGHT_VERSION);
write_file(dir.join("Cargo.toml"), &cargo_toml)?;
write_file(dir.join("src/main.rs"), GUEST_MAIN_RS)?;
Ok(())
}

fn write_host(dir: &Path, name: &str, guest_name: &str) -> Result<()> {
let cargo_toml = HOST_CARGO_TOML
.replace("{name}", name)
.replace("{version}", HYPERLIGHT_VERSION);
let main_rs = HOST_MAIN_RS
.replace("{name}", name)
.replace("{guest_name}", guest_name)
.replace("{arch}", GUEST_ARCH);
write_file(dir.join("Cargo.toml"), &cargo_toml)?;
write_file(dir.join("src/main.rs"), &main_rs)?;
Ok(())
}

fn write_file(path: impl AsRef<Path>, content: &str) -> Result<()> {
let path = path.as_ref();
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)
.with_context(|| format!("Failed to create directory '{}'", parent.display()))?;
}
fs::write(path, content).with_context(|| format!("Failed to write '{}'", path.display()))?;
Ok(())
}

/// Validate that the name is usable as a Cargo package name.
/// Mirrors the essential checks from `cargo new`.
fn validate_name(name: &str) -> Result<()> {
ensure!(!name.is_empty(), "project name must not be empty");
ensure!(
name.chars()
.all(|c| c.is_alphanumeric() || c == '-' || c == '_'),
"invalid project name `{name}`: must contain only letters, numbers, `-`, or `_`"
);
ensure!(
name.chars()
.next()
.is_some_and(|c| c.is_alphabetic() || c == '_'),
"invalid project name `{name}`: must start with a letter or `_`"
);
let reserved = [
"test",
"core",
"std",
"alloc",
"proc_macro",
"proc-macro",
"self",
"Self",
"crate",
"super",
];
ensure!(
!reserved.contains(&name),
"invalid project name `{name}`: it conflicts with a Rust built-in name"
);
Ok(())
}
Loading
Loading