Building a Rust Game Engine: Part 2 - Entry Point
Last time we set up the project structure. Now we have a workspace with an engine library, an editor, and a sandbox. But there’s a problem: each application defines its own main() function.
// sandbox/src/main.rs
fn main() {
engine::init();
println!("Sandbox started");
engine::run();
engine::shutdown();
}
This works, but it means the application controls the entry point. For an engine, we want that flipped—the engine should own the entry point and call into the application.
Why Engine-Owned Entry Points?
When the engine controls main(), it can:
- Guarantee initialization order (logging, memory, subsystems)
- Handle platform-specific startup (WinMain on Windows, etc.)
- Ensure proper shutdown even if the app crashes
- Set up profiling, crash handlers, and debugging infrastructure
The application just says “here’s my app” and the engine handles the rest.
The C++ Approach
In C++ engines like Hazel, this is typically done with header tricks:
// engine/entry_point.h
extern Application* CreateApplication();
int main() {
auto app = CreateApplication();
app->Run();
delete app;
}
The client implements CreateApplication(), includes the header, and the linker figures it out. Clever, but relies on extern linkage and header inclusion order.
The Rust Approach
Rust doesn’t have headers or extern in the same way. Instead, we use traits and macros.
The Application Trait
First, define what an application looks like:
// engine/src/application.rs
pub trait Application {
/// Create a new instance of the application
fn new() -> Self where Self: Sized;
/// Run the application main loop
fn run(&mut self);
}
Simple. The engine doesn’t care what your app does—it just needs to create it and run it.
The Entry Point Macro
Now the magic. We’ll generate main() for the client:
// engine/src/entrypoint.rs
use crate::Application;
pub fn run_application<T: Application>() {
crate::init();
let mut app = T::new();
app.run();
crate::shutdown();
}
#[macro_export]
macro_rules! entrypoint {
($app:ty) => {
fn main() {
$crate::run_application::<$app>();
}
};
}
The $crate prefix is important—it resolves to the engine crate regardless of how the macro is invoked. Without it, the generated code would try to find run_application in the client’s scope.
Wiring It Up
The engine’s lib.rs exports everything:
// engine/src/lib.rs
mod application;
mod entrypoint;
pub use application::Application;
pub use entrypoint::run_application;
pub fn init() {
println!("GGEngine initialized");
}
pub fn shutdown() {
println!("GGEngine shutdown");
}
Client Code
Now the sandbox becomes beautifully simple:
// sandbox/src/main.rs
use engine::Application;
struct Sandbox;
impl Application for Sandbox {
fn new() -> Self {
println!("Sandbox created");
Sandbox
}
fn run(&mut self) {
loop {
// Main loop - will add proper logic later
}
}
}
engine::entrypoint!(Sandbox);
No fn main(). The macro generates it. The application just defines itself and declares the entry point in one line.
Running this:
$ cargo run --bin sandbox
GGEngine initialized
Sandbox created
<infinite loop>
The engine initializes first, then creates the app, then runs it. Exactly what we wanted.
Why a Macro?
You might wonder why not just have the client write:
fn main() {
engine::run::<Sandbox>();
}
That works too. But the macro approach has advantages:
- Consistency - Every GGEngine app looks the same
- Future flexibility - We can change what
main()does without touching client code - Platform abstraction - On Windows, we might want
WinMaininstead ofmain; the macro can handle that
For now, the macro is simple. But it’s a hook for future complexity.
The Flow
entrypoint!(Sandbox)
└── generates main()
└── run_application::<Sandbox>()
├── init()
├── Sandbox::new()
├── app.run()
└── shutdown()
The engine owns the lifecycle. The app just exists within it.
What’s Next
We have an entry point and an infinite loop. Not exactly exciting. Next up: logging.
This is part 2 of a series on building a game engine in Rust.