Skip to main content

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, Symbol};
17
18use crate::{
19    CliError, LibSourceSpec, LoadSession, RuntimeConfigState, parse_args, run_command_with_session,
20};
21
22/// A thin bootloader over [`LoadSession`] for a single-library product binary.
23///
24/// ```no_run
25/// # use sim_run_core::Bootloader;
26/// # struct MyServeLib;
27/// # impl MyServeLib { fn new() -> Self { Self } }
28/// # impl sim_kernel::Lib for MyServeLib {
29/// #     fn manifest(&self) -> sim_kernel::LibManifest { unimplemented!() }
30/// #     fn load(&self, _: &mut sim_kernel::LoadCx, _: &mut sim_kernel::Linker<'_>)
31/// #         -> sim_kernel::Result<()> { Ok(()) }
32/// # }
33/// // `my-server ARGS...` boots exactly like `sim serve ARGS...`, dispatching the
34/// // library's `cli/main/serve` entrypoint. No Cx::new in the binary.
35/// let code = Bootloader::standard()
36///     .host_verb("serve", "lib/my-server", || Box::new(MyServeLib::new()))
37///     .run(std::iter::once("serve".into()).chain(std::env::args_os().skip(1)))?;
38/// std::process::exit(code);
39/// # Ok::<(), sim_run_core::CliError>(())
40/// ```
41pub struct Bootloader {
42    session: LoadSession,
43}
44
45impl Bootloader {
46    /// The standard boot session: the in-process host loader only, matching the
47    /// default `sim` binary. Add libraries with [`Bootloader::host_verb`].
48    pub fn standard() -> Self {
49        Self {
50            session: LoadSession::new(),
51        }
52    }
53
54    /// Applies a reusable host composition to the underlying load session.
55    ///
56    /// Product libraries use this when a standalone product binary and the
57    /// standard `sim` distribution must install exactly the same host
58    /// factories, capabilities, context support, and runtime configuration.
59    pub fn configure_session(self, configure: impl FnOnce(LoadSession) -> LoadSession) -> Self {
60        Self {
61            session: configure(self.session),
62        }
63    }
64
65    /// Registers a statically-linked library under `name` and makes it the default
66    /// source for `verb`, so a bare `<verb> ARGS...` dispatches to the library's
67    /// `cli/main/<verb>` entrypoint with no explicit `--load`.
68    ///
69    /// The library must be a [`LibTarget::HostRegistered`](sim_kernel::LibTarget)
70    /// lib that exports a `cli/main/<verb>` function (see
71    /// [`crate::cli_main_entrypoint_symbol`]).
72    pub fn host_verb<F>(self, verb: &str, name: &str, factory: F) -> Self
73    where
74        F: Fn() -> Box<dyn Lib> + Send + Sync + 'static,
75    {
76        Self {
77            session: self
78                .session
79                .with_host_factory(name.to_owned(), factory)
80                .with_default_verb_sources(
81                    verb.to_owned(),
82                    vec![LibSourceSpec::Host(name.to_owned())],
83                ),
84        }
85    }
86
87    /// Registers a statically-linked library under `name`, makes it the default
88    /// source for `verb`, and reads the supplied config library ids before the
89    /// host library is instantiated.
90    pub fn host_verb_with_config<F>(
91        self,
92        verb: &str,
93        name: &str,
94        config_libs: Vec<Symbol>,
95        factory: F,
96    ) -> Self
97    where
98        F: Fn(&RuntimeConfigState) -> Box<dyn Lib> + Send + Sync + 'static,
99    {
100        Self {
101            session: self
102                .session
103                .with_host_factory_with_config(name.to_owned(), factory)
104                .with_default_verb_sources(
105                    verb.to_owned(),
106                    vec![LibSourceSpec::Host(name.to_owned())],
107                )
108                .with_default_verb_config_libs(verb.to_owned(), config_libs),
109        }
110    }
111
112    /// Registers a statically-linked library under `name` WITHOUT binding it to a
113    /// verb -- for a supporting library the served verb needs, most commonly the boot
114    /// codec (register it under `codec/<name>` and pass `--codec <name>` so the boot
115    /// resolves the host codec instead of the default).
116    pub fn host_lib<F>(self, name: &str, factory: F) -> Self
117    where
118        F: Fn() -> Box<dyn Lib> + Send + Sync + 'static,
119    {
120        Self {
121            session: self.session.with_host_factory(name.to_owned(), factory),
122        }
123    }
124
125    /// Grants a capability the served library requires (for example a transport or
126    /// tool-call capability).
127    pub fn with_capability(self, capability: CapabilityName) -> Self {
128        Self {
129            session: self.session.with_capability(capability),
130        }
131    }
132
133    /// Installs context-level runtime support into the boot `Cx` before dispatch
134    /// (e.g. a codec, an eval policy, a supporting lib). Concrete serve behavior
135    /// stays in the loaded library.
136    pub fn with_context<F>(self, configure: F) -> Self
137    where
138        F: FnOnce(&mut Cx),
139    {
140        Self {
141            session: self.session.with_context(configure),
142        }
143    }
144
145    /// Boots the runtime, applies the registered libraries, and dispatches `args`
146    /// (typically the process arguments). A blocking serve verb runs to completion
147    /// here; its returned value becomes the process exit code.
148    pub fn run<I, S>(self, args: I) -> Result<i32, CliError>
149    where
150        I: IntoIterator<Item = S>,
151        S: Into<OsString>,
152    {
153        let mut session = self.session;
154        let command = parse_args(args)?;
155        run_command_with_session(command, &mut session)
156    }
157}