Skip to content

Hello World (Rust)

This guide creates a minimal Electrobun app with a Rust main process in a project-owned Cargo package. For the default TypeScript runtime see Hello World. The faster path is hutch electrobun init rust-app --template=rust-flock-wgpu, which scaffolds a complete WGPU-rendering example.

1. Create the project

Terminal window
mkdir my-rust-app
cd my-rust-app

Create hutch.config.ts. Hutch resolves scripts only from this file; this project does not need a package.json:

export default {
electrobun: { version: "2.0.1-beta.0" },
scripts: {
dev: ["hutch", "electrobun", "dev", "--watch"],
build: ["hutch", "electrobun", "build", "--env=stable"],
},
};

This project does not need package.json. The exact Electrobun release in hutch.config.ts selects an installed devkit, and Hutch copies its Rust SDK to .hutch/devkit/rust-sdk.

Create tsconfig.json so editors and TypeScript resolve the config types and view SDK projected during sync:

{
"extends": "./.hutch/devkit/tsconfig.json"
}

2. Add a webview

Create src/mainview/index.html:

<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width" />
<title>Hello Electrobun</title>
</head>
<body>
<h1>Hello from Electrobun</h1>
<script type="module" src="index.js"></script>
</body>
</html>

Create src/mainview/index.ts — the view’s script entrypoint, bundled to views://mainview/index.js:

console.log("Hello from the view");

3. Add the main process

Create Cargo.toml:

[package]
name = "rust-hello"
version = "0.0.1"
edition = "2021"
publish = false
[[bin]]
name = "main"
path = "src/main.rs"
[dependencies]
electrobun = { path = ".hutch/devkit/rust-sdk" }
[profile.dev]
opt-level = 2
debug = 0
[profile.release]
opt-level = "z"
strip = "symbols"

That path is the Rust SDK copied from the exact Electrobun devkit. Hutch does not publish or inject a hidden crate and does not generate the manifest. Add normal crates here and let Cargo own their versions and lockfile.

Create and commit Cargo.lock (the SDK version matches the electrobun.version in hutch.config.ts):

# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "electrobun"
version = "2.0.0"
[[package]]
name = "rust-hello"
version = "0.0.1"
dependencies = [
"electrobun",
]

Create src/main.rs:

use electrobun::{
self, Core, Rect, WebviewCallbacks, WebviewOptions, WindowCallbacks, WindowOptions,
};
use std::sync::OnceLock;
use std::thread;
use std::time::Duration;
// 32 comma-separated bytes; shared secret for the webview preload bridge.
const DEFAULT_SECRET_KEY: &str =
"1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32";
static CORE: OnceLock<&'static Core> = OnceLock::new();
fn main() {
if let Err(err) = run() {
eprintln!("[rust-hello] {err}");
std::process::exit(1);
}
}
fn run() -> Result<(), String> {
let core = Box::leak(Box::new(Core::load()?));
let bundle_paths = electrobun::resolve_bundle_paths()?;
let app_info = electrobun::resolve_app_info_from_bundle(&bundle_paths)?;
CORE.set(core).map_err(|_| "core already set".to_string())?;
// UI must be created after the event loop starts, so spawn a thread.
let _ui_thread = thread::spawn(move || create_ui(core, &bundle_paths));
// Blocks on the native event loop; must run on the main thread.
core.run_main_thread(&app_info)
}
fn create_ui(core: &Core, bundle_paths: &electrobun::BundlePaths) {
thread::sleep(Duration::from_millis(150)); // let the event loop start
if let Err(err) = core.configure_webview_runtime_from_executable_dir(bundle_paths, 0) {
eprintln!("[rust-hello] failed to configure webview runtime: {err}");
return;
}
let mut window_options =
WindowOptions::new("Hello Electrobun", Rect::new(140.0, 100.0, 900.0, 640.0));
window_options.callbacks = WindowCallbacks {
close: Some(main_window_closed),
..WindowCallbacks::default()
};
let window_id = match core.create_window(window_options) {
Ok(id) => id,
Err(err) => {
eprintln!("[rust-hello] failed to create window: {err}");
return;
}
};
let mut webview_options = WebviewOptions::new(
window_id,
"views://mainview/index.html",
Rect::new(0.0, 0.0, 900.0, 640.0),
);
webview_options.secret_key = DEFAULT_SECRET_KEY;
webview_options.callbacks = WebviewCallbacks {
decide_navigation: Some(electrobun::allow_all_navigation),
..WebviewCallbacks::default()
};
if let Err(err) = core.create_webview(webview_options) {
eprintln!("[rust-hello] failed to create webview: {err}");
let _ = core.close_window(window_id);
}
}
extern "C" fn main_window_closed(_window_id: u32) {
if let Some(core) = CORE.get() {
let _ = core.stop_event_loop();
}
}

A few things to notice: Box::leak produces the &'static Core that extern "C" callbacks need, the secret key is shared with the webview’s preload bridge, and the window’s close callback stops the event loop so the process exits when the window closes.

4. Configure the build

Create electrobun.config.ts:

import type { ElectrobunConfig } from "electrobun";
export default {
app: {
name: "rust-hello",
identifier: "rusthello.electrobun.dev",
version: "0.0.1",
},
build: {
mainProcess: "rust",
rust: {
manifest: "Cargo.toml",
binary: "main",
},
views: {
mainview: {
entrypoint: "src/mainview/index.ts",
},
},
copy: {
"src/mainview/index.html": "views/mainview/index.html",
},
},
} satisfies ElectrobunConfig;

mainProcess: "rust" selects the Rust runtime. manifest and binary choose the project-owned Cargo target; those shown are the defaults. Per-platform bundleCEF / bundleWGPU flags are only needed if you use CEF rendering or WGPU views.

5. Run the app

Terminal window
hutch electrobun sync
hutch run dev

The explicit sync creates .hutch/devkit/rust-sdk and resolves the selected Rust toolchain if no exact match is installed. hutch run dev then builds with Cargo’s locked dependency graph, launches the app, and rebuilds when watched source files change. Press Ctrl+C to stop it.

How this differs from TypeScript

  • Normal Cargo ownership. The project owns Cargo.toml, Cargo.lock, its binary targets, and crates.io dependencies. Hutch runs Cargo with the selected toolchain and --locked; it does not generate a wrapper crate.
  • Version-matched SDK. hutch electrobun sync projects the selected release’s Rust crate into .hutch/devkit/rust-sdk, and the normal Cargo path dependency makes it part of the build.
  • Dynamic core loading. Core::load() dlopens libElectrobunCore from the executable’s directory at runtime and resolves its symbols. Nothing links at build time, and the binary only runs from inside a Hutch-built bundle. cargo build can compile the binary, but the executable needs the packaged core and Resources layout to run correctly.
  • Bundle discovery. resolve_bundle_paths() derives the executable and Resources/ directories from the running executable’s path, and resolve_app_info_from_bundle() reads the app identifier, name, and channel from Resources/version.json.
  • Webview runtime setup. Call configure_webview_runtime_from_executable_dir once before creating any webview; it registers the preload scripts that power the views:// scheme and the bridge.
  • You own the event loop. run_main_thread must be called on the process main thread and blocks until the loop stops. Create windows and webviews from a spawned thread after a short delay (~150 ms) so the loop is running first, and call stop_event_loop from the window close callback or the process keeps running.
  • Errors are Result<_, String>. Every core call returns a Result; the error strings come from the core’s electrobun_core_last_error.
  • Template. hutch electrobun init rust-app --template=rust-flock-wgpu scaffolds a full example with RPC messaging and WGPU rendering.

Toolchain

Electrobun 2.0.0 defaults to Rust 1.88.0. Override it exactly with build.rust.version; Hutch uses an exact matching system compiler when supported or downloads the toolchain under ~/.hutch/toolchains/rust/<version>/<platform>. Builds target the host OS and architecture only — cross-compilation is not supported yet, so use native runners per platform. On Windows, Rust builds require Visual Studio Build Tools and the Windows SDK; macOS needs the Xcode Command Line Tools.

For the full picture of native main processes, see Native main process and the main process APIs.