sim_run_core/bootloader.rs
1//! A reusable bootloader for a binary that ships one statically-linked library and
2//! boots it through the same [`LoadSession`] machinery the `sim` binary uses.
3//!
4//! Server binaries (MCP, web) used to hand-roll their own runtime
5//! (`Cx::new(...)` + codec/lib loads + a transport loop). That is divergent boot:
6//! a fix to the runtime bootstrap in one place never reaches the others. Instead,
7//! every product binary composes a [`Bootloader`], registers its serve library as a
8//! host factory, and dispatches the library's `cli/main/<verb>` entrypoint -- the
9//! exact path `sim --load host:<lib> <verb>` follows. No binary constructs its own
10//! `Cx`.
11//!
12//! The pattern mirrors the interactive REPL boot (`sim repl`): a host-registered
13//! library exports a `cli/main/<verb>` function whose [`Callable`] runs the (possibly
14//! long-lived, blocking) serve loop and returns a truthy value at shutdown, which the
15//! bootloader maps to the process exit code.
16
17use std::ffi::OsString;
18
19use sim_kernel::{CapabilityName, Cx, Lib};
20
21use crate::{CliError, LibSourceSpec, LoadSession, parse_args, run_command_with_session};
22
23/// A thin bootloader over [`LoadSession`] for a single-library product binary.
24///
25/// ```no_run
26/// # use sim_run_core::Bootloader;
27/// # struct MyServeLib;
28/// # impl MyServeLib { fn new() -> Self { Self } }
29/// # impl sim_kernel::Lib for MyServeLib {
30/// # fn manifest(&self) -> sim_kernel::LibManifest { unimplemented!() }
31/// # fn load(&self, _: &mut sim_kernel::LoadCx, _: &mut sim_kernel::Linker<'_>)
32/// # -> sim_kernel::Result<()> { Ok(()) }
33/// # }
34/// // `my-server ARGS...` boots exactly like `sim serve ARGS...`, dispatching the
35/// // library's `cli/main/serve` entrypoint. No Cx::new in the binary.
36/// let code = Bootloader::standard()
37/// .host_verb("serve", "lib/my-server", || Box::new(MyServeLib::new()))
38/// .run(std::iter::once("serve".into()).chain(std::env::args_os().skip(1)))?;
39/// std::process::exit(code);
40/// # Ok::<(), sim_run_core::CliError>(())
41/// ```
42pub struct Bootloader {
43 session: LoadSession,
44}
45
46impl Bootloader {
47 /// The standard boot session: the in-process host loader only, matching the
48 /// default `sim` binary. Add libraries with [`Bootloader::host_verb`].
49 pub fn standard() -> Self {
50 Self {
51 session: LoadSession::new(),
52 }
53 }
54
55 /// Registers a statically-linked library under `name` and makes it the default
56 /// source for `verb`, so a bare `<verb> ARGS...` dispatches to the library's
57 /// `cli/main/<verb>` entrypoint with no explicit `--load`.
58 ///
59 /// The library must be a [`LibTarget::HostRegistered`](sim_kernel::LibTarget)
60 /// lib that exports a `cli/main/<verb>` function (see
61 /// [`crate::cli_main_entrypoint_symbol`]).
62 pub fn host_verb<F>(self, verb: &str, name: &str, factory: F) -> Self
63 where
64 F: Fn() -> Box<dyn Lib> + Send + Sync + 'static,
65 {
66 Self {
67 session: self
68 .session
69 .with_host_factory(name.to_owned(), factory)
70 .with_default_verb_sources(
71 verb.to_owned(),
72 vec![LibSourceSpec::Host(name.to_owned())],
73 ),
74 }
75 }
76
77 /// Registers a statically-linked library under `name` WITHOUT binding it to a
78 /// verb -- for a supporting library the served verb needs, most commonly the boot
79 /// codec (register it under `codec/<name>` and pass `--codec <name>` so the boot
80 /// resolves the host codec instead of the default).
81 pub fn host_lib<F>(self, name: &str, factory: F) -> Self
82 where
83 F: Fn() -> Box<dyn Lib> + Send + Sync + 'static,
84 {
85 Self {
86 session: self.session.with_host_factory(name.to_owned(), factory),
87 }
88 }
89
90 /// Grants a capability the served library requires (for example a transport or
91 /// tool-call capability).
92 pub fn with_capability(self, capability: CapabilityName) -> Self {
93 Self {
94 session: self.session.with_capability(capability),
95 }
96 }
97
98 /// Installs context-level runtime support into the boot `Cx` before dispatch
99 /// (e.g. a codec, an eval policy, a supporting lib). Concrete serve behavior
100 /// stays in the loaded library.
101 pub fn with_context<F>(self, configure: F) -> Self
102 where
103 F: FnOnce(&mut Cx),
104 {
105 Self {
106 session: self.session.with_context(configure),
107 }
108 }
109
110 /// Boots the runtime, applies the registered libraries, and dispatches `args`
111 /// (typically the process arguments). A blocking serve verb runs to completion
112 /// here; its returned value becomes the process exit code.
113 pub fn run<I, S>(self, args: I) -> Result<i32, CliError>
114 where
115 I: IntoIterator<Item = S>,
116 S: Into<OsString>,
117 {
118 let mut session = self.session;
119 let command = parse_args(args)?;
120 run_command_with_session(command, &mut session)
121 }
122}