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