Skip to content

Commit ebf39c0

Browse files
committed
Add scaffold subcommand
Signed-off-by: Ludvig Liljenberg <4257730+ludfjig@users.noreply.github.com>
1 parent 14269f7 commit ebf39c0

File tree

10 files changed

+392
-1
lines changed

10 files changed

+392
-1
lines changed

.github/workflows/ci.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,9 @@ jobs:
4141
- name: Run example
4242
shell: bash
4343
run: just run-guest
44+
- name: Test scaffold
45+
shell: bash
46+
run: just test-scaffold
4447

4548
spelling:
4649
name: Spell check with typos

justfile

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,15 @@ fmt:
77
cargo +nightly fmt --all -- --check
88
cargo +nightly fmt --all --manifest-path ./examples/host/Cargo.toml -- --check
99
cargo +nightly fmt --all --manifest-path ./examples/guest/Cargo.toml -- --check
10+
# These are standalone template files not part of any crate, so cargo fmt wont find them.
11+
rustfmt +nightly --check ./src/scaffold/guest/_main.rs ./src/scaffold/host/_main.rs
1012

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

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

2428
run-guest: build-guest
25-
cargo run --manifest-path ./examples/host/Cargo.toml -- ./target/x86_64-hyperlight-none/debug/guest
29+
cargo run --manifest-path ./examples/host/Cargo.toml -- ./target/x86_64-hyperlight-none/debug/guest
30+
31+
test-scaffold:
32+
cargo test --test scaffold

src/main.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ use std::env;
33
use cargo_hyperlight::cargo;
44

55
mod perf;
6+
mod scaffold;
67

78
const VERSION: &str = env!("CARGO_PKG_VERSION");
89
const GIT_HASH: &str = env!("GIT_HASH");
@@ -26,6 +27,12 @@ fn main() {
2627
std::process::exit(1);
2728
}
2829
}
30+
Some(a) if a == "scaffold" => {
31+
if let Err(e) = scaffold::run(args) {
32+
eprintln!("{e:?}");
33+
std::process::exit(1);
34+
}
35+
}
2936
_ => {
3037
cargo()
3138
.expect("Failed to create cargo command")

src/scaffold/_gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
target

src/scaffold/guest/_Cargo.toml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
[package]
2+
name = "{name}"
3+
version = "0.1.0"
4+
edition = "2024"
5+
6+
[dependencies]
7+
hyperlight-guest = "{version}"
8+
hyperlight-guest-bin = "{version}"
9+
hyperlight-common = { version = "{version}", default-features = false }

src/scaffold/guest/_main.rs

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
#![no_std]
2+
#![no_main]
3+
extern crate alloc;
4+
extern crate hyperlight_guest_bin;
5+
6+
use alloc::string::String;
7+
use alloc::vec::Vec;
8+
use core::sync::atomic::{AtomicI32, Ordering};
9+
10+
use hyperlight_common::flatbuffer_wrappers::function_call::FunctionCall;
11+
use hyperlight_common::flatbuffer_wrappers::guest_error::ErrorCode;
12+
use hyperlight_guest::bail;
13+
use hyperlight_guest::error::Result;
14+
use hyperlight_guest_bin::{guest_function, host_function};
15+
16+
static COUNTER: AtomicI32 = AtomicI32::new(0);
17+
18+
// Declare a host function that the guest can call. The string is the
19+
// registration name (must match what the host passes to register()).
20+
// If omitted, the Rust function name is used.
21+
// The host must register this before the sandbox is initialized.
22+
#[host_function("GetWeekday")]
23+
fn get_weekday() -> Result<String>;
24+
25+
// Register a guest function that can be called by the host.
26+
#[guest_function("SayHello")]
27+
fn say_hello(name: String) -> Result<String> {
28+
let weekday = get_weekday()?;
29+
Ok(alloc::format!("Hello, {name}! Today is {weekday}."))
30+
}
31+
32+
// Guest functions can take multiple arguments of different types.
33+
#[guest_function("Add")]
34+
fn add(a: i32, b: i32) -> Result<i32> {
35+
Ok(a + b)
36+
}
37+
38+
// Increments a counter and returns the new value. State persists across
39+
// calls until the host restores a snapshot, which resets all VM memory
40+
// back to the state it was in when the snapshot was taken.
41+
#[guest_function("Increment")]
42+
fn increment() -> Result<i32> {
43+
COUNTER.fetch_add(1, Ordering::Relaxed);
44+
Ok(COUNTER.load(Ordering::Relaxed))
45+
}
46+
47+
// Called once when the guest binary is loaded, during evolve().
48+
// Use this for initialization.
49+
#[unsafe(no_mangle)]
50+
pub extern "C" fn hyperlight_main() {}
51+
52+
// Called when the host calls a guest function not handled by #[guest_function].
53+
// You usually don't need to modify this.
54+
#[unsafe(no_mangle)]
55+
pub fn guest_dispatch_function(function_call: FunctionCall) -> Result<Vec<u8>> {
56+
let function_name = function_call.function_name;
57+
bail!(ErrorCode::GuestFunctionNotFound => "{function_name}");
58+
}

src/scaffold/host/_Cargo.toml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
[package]
2+
name = "{name}"
3+
version = "0.1.0"
4+
edition = "2024"
5+
6+
[dependencies]
7+
hyperlight-host = "{version}"

src/scaffold/host/_main.rs

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
use std::path::PathBuf;
2+
use std::time::{SystemTime, UNIX_EPOCH};
3+
4+
use hyperlight_host::{GuestBinary, MultiUseSandbox, UninitializedSandbox};
5+
6+
fn main() -> hyperlight_host::Result<()> {
7+
// TODO: support aarch64-hyperlight-none when aarch64 guests are supported.
8+
let base = PathBuf::from("../guest/target/x86_64-hyperlight-none");
9+
let guest_path = ["debug", "release"]
10+
.iter()
11+
.map(|p| base.join(p).join("{guest_name}"))
12+
.find(|p| p.exists())
13+
.expect(
14+
"guest binary not found - build it first with: cd ../guest && cargo hyperlight build",
15+
);
16+
17+
// Create a sandbox from the guest binary. It starts uninitialized so you
18+
// can register host functions before the guest begins executing.
19+
let mut sandbox = UninitializedSandbox::new(
20+
GuestBinary::FilePath(guest_path.display().to_string()),
21+
None,
22+
)?;
23+
24+
// Register a host function that the guest can call.
25+
sandbox.register("GetWeekday", weekday)?;
26+
27+
// Evolve into a MultiUseSandbox, which lets you call guest functions
28+
// multiple times.
29+
let mut sandbox: MultiUseSandbox = sandbox.evolve()?;
30+
31+
// Call a guest function with a single argument.
32+
let result: String = sandbox.call("SayHello", "World".to_string())?;
33+
println!("{result}");
34+
35+
// Multiple arguments are passed as a tuple.
36+
let sum: i32 = sandbox.call("Add", (2_i32, 3_i32))?;
37+
println!("2 + 3 = {sum}");
38+
39+
// Guest state persists between calls. Take a snapshot so we can
40+
// restore back to this point later.
41+
let snapshot = sandbox.snapshot()?;
42+
43+
let count: i32 = sandbox.call("Increment", ())?;
44+
println!("count = {count}"); // 1
45+
let count: i32 = sandbox.call("Increment", ())?;
46+
println!("count = {count}"); // 2
47+
let count: i32 = sandbox.call("Increment", ())?;
48+
println!("count = {count}"); // 3
49+
50+
// Restore resets all guest memory back to the snapshot.
51+
sandbox.restore(snapshot)?;
52+
53+
let count: i32 = sandbox.call("Increment", ())?;
54+
println!("count after restore = {count}"); // 1 again
55+
56+
Ok(())
57+
}
58+
59+
// Returns the current day of the week as a String.
60+
fn weekday() -> hyperlight_host::Result<String> {
61+
let days = [
62+
"Monday",
63+
"Tuesday",
64+
"Wednesday",
65+
"Thursday",
66+
"Friday",
67+
"Saturday",
68+
"Sunday",
69+
];
70+
let secs = SystemTime::now()
71+
.duration_since(UNIX_EPOCH)
72+
.expect("system clock before Unix epoch")
73+
.as_secs();
74+
// January 1, 1970 was a Thursday (day index 3 when Monday = 0).
75+
Ok(days[((secs / (60 * 60 * 24) + 3) % 7) as usize].to_string())
76+
}

src/scaffold/mod.rs

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
use std::ffi::OsString;
2+
use std::fs;
3+
use std::path::{Path, PathBuf};
4+
5+
use anyhow::{Context, Result, ensure};
6+
use clap::Parser;
7+
8+
const HYPERLIGHT_VERSION: &str = "0.13";
9+
10+
const GUEST_CARGO_TOML: &str = include_str!("guest/_Cargo.toml");
11+
const GUEST_MAIN_RS: &str = include_str!("guest/_main.rs");
12+
const HOST_CARGO_TOML: &str = include_str!("host/_Cargo.toml");
13+
const HOST_MAIN_RS: &str = include_str!("host/_main.rs");
14+
const GITIGNORE: &str = include_str!("_gitignore");
15+
16+
/// Scaffold a new Hyperlight project.
17+
#[derive(Parser, Debug)]
18+
#[command(name = "scaffold")]
19+
struct ScaffoldCli {
20+
/// Path to create the project at. The directory name is used as the crate
21+
/// name (like `cargo new`).
22+
path: PathBuf,
23+
24+
/// Generate only a guest project instead of both host and guest.
25+
#[arg(long, default_value_t = false)]
26+
guest_only: bool,
27+
}
28+
29+
pub fn run(args: impl Iterator<Item = OsString>) -> Result<()> {
30+
let cli = ScaffoldCli::parse_from(args);
31+
32+
let name = cli
33+
.path
34+
.file_name()
35+
.context("Invalid project path")?
36+
.to_str()
37+
.context("Project name must be valid UTF-8")?;
38+
39+
ensure!(!name.is_empty(), "Project name must not be empty");
40+
ensure!(
41+
!cli.path.exists(),
42+
"Directory '{}' already exists",
43+
cli.path.display()
44+
);
45+
46+
if cli.guest_only {
47+
write_guest(&cli.path, name)?;
48+
} else {
49+
let guest_name = format!("{name}-guest");
50+
write_guest(&cli.path.join("guest"), &guest_name)?;
51+
write_host(&cli.path.join("host"), &format!("{name}-host"), &guest_name)?;
52+
}
53+
write_file(cli.path.join(".gitignore"), GITIGNORE)?;
54+
55+
let dir = cli.path.display();
56+
println!("Created project at '{dir}'\n");
57+
if cli.guest_only {
58+
println!("Build:");
59+
println!(" cd {dir} && cargo hyperlight build");
60+
} else {
61+
println!("Build and run:");
62+
println!(" cd {dir}/guest && cargo hyperlight build");
63+
println!(" cd {dir}/host && cargo run");
64+
}
65+
66+
Ok(())
67+
}
68+
69+
fn write_guest(dir: &Path, name: &str) -> Result<()> {
70+
let cargo_toml = GUEST_CARGO_TOML
71+
.replace("{name}", name)
72+
.replace("{version}", HYPERLIGHT_VERSION);
73+
write_file(dir.join("Cargo.toml"), &cargo_toml)?;
74+
write_file(dir.join("src/main.rs"), GUEST_MAIN_RS)?;
75+
Ok(())
76+
}
77+
78+
fn write_host(dir: &Path, name: &str, guest_name: &str) -> Result<()> {
79+
let cargo_toml = HOST_CARGO_TOML
80+
.replace("{name}", name)
81+
.replace("{version}", HYPERLIGHT_VERSION);
82+
let main_rs = HOST_MAIN_RS
83+
.replace("{name}", name)
84+
.replace("{guest_name}", guest_name);
85+
write_file(dir.join("Cargo.toml"), &cargo_toml)?;
86+
write_file(dir.join("src/main.rs"), &main_rs)?;
87+
Ok(())
88+
}
89+
90+
fn write_file(path: impl AsRef<Path>, content: &str) -> Result<()> {
91+
let path = path.as_ref();
92+
if let Some(parent) = path.parent() {
93+
fs::create_dir_all(parent)
94+
.with_context(|| format!("Failed to create directory '{}'", parent.display()))?;
95+
}
96+
fs::write(path, content).with_context(|| format!("Failed to write '{}'", path.display()))?;
97+
Ok(())
98+
}

0 commit comments

Comments
 (0)