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
mkdir my-rust-appcd my-rust-appCreate 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 = 2debug = 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
hutch electrobun synchutch run devThe 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 syncprojects 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()dlopenslibElectrobunCorefrom 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 buildcan compile the binary, but the executable needs the packaged core and Resources layout to run correctly. - Bundle discovery.
resolve_bundle_paths()derives the executable andResources/directories from the running executable’s path, andresolve_app_info_from_bundle()reads the app identifier, name, and channel fromResources/version.json. - Webview runtime setup. Call
configure_webview_runtime_from_executable_dironce before creating any webview; it registers the preload scripts that power theviews://scheme and the bridge. - You own the event loop.
run_main_threadmust 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 callstop_event_loopfrom the window close callback or the process keeps running. - Errors are
Result<_, String>. Every core call returns aResult; the error strings come from the core’selectrobun_core_last_error. - Template.
hutch electrobun init rust-app --template=rust-flock-wgpuscaffolds 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.