Skip to main content

bern_target_api/
glue.rs

1//! Rust source for generated glue crates.
2//!
3//! Targets that link the engine and the session runtime into a product
4//! generate a small crate around `bern-ffi`. These helpers write the IR's
5//! schema and session description as Rust expressions, so every target
6//! describes the same session layer the same way.
7
8use bern_ir::Application;
9use bern_protocol::{EnumDef, FieldDef, RecordDef, Type, TypeTable, Value};
10use std::path::Path;
11
12const P: &str = "bern_ffi::protocol";
13
14pub fn type_expr(ty: &Type) -> String {
15    match ty {
16        Type::Unit => format!("{P}::Type::Unit"),
17        Type::Bool => format!("{P}::Type::Bool"),
18        Type::Int => format!("{P}::Type::Int"),
19        Type::Float => format!("{P}::Type::Float"),
20        Type::Text => format!("{P}::Type::Text"),
21        Type::Enum { name } => format!("{P}::Type::Enum {{ name: {name:?}.into() }}"),
22        Type::Record { name } => format!("{P}::Type::Record {{ name: {name:?}.into() }}"),
23        Type::Optional { inner } => format!("{P}::Type::optional({})", type_expr(inner)),
24        Type::List { inner } => format!("{P}::Type::list({})", type_expr(inner)),
25    }
26}
27
28pub fn value_expr(value: &Value) -> String {
29    let list = |values: &[Value]| values.iter().map(value_expr).collect::<Vec<_>>().join(", ");
30    match value {
31        Value::Unit => format!("{P}::Value::Unit"),
32        Value::Bool(value) => format!("{P}::Value::Bool({value})"),
33        Value::Int(value) => format!("{P}::Value::Int({value})"),
34        Value::Float(value) => format!("{P}::Value::Float({value:?})"),
35        Value::Text(value) => format!("{P}::Value::Text({value:?}.into())"),
36        Value::Enum { variant, fields } => format!(
37            "{P}::Value::Enum {{ variant: {variant}, fields: vec![{}] }}",
38            list(fields)
39        ),
40        Value::Optional(None) => format!("{P}::Value::Optional(None)"),
41        Value::Optional(Some(value)) => format!("{P}::Value::some({})", value_expr(value)),
42        Value::Record(values) => format!("{P}::Value::Record(vec![{}])", list(values)),
43        Value::List(values) => format!("{P}::Value::List(vec![{}])", list(values)),
44    }
45}
46
47fn field_expr(field: &FieldDef) -> String {
48    format!(
49        "{P}::FieldDef {{ name: {:?}.into(), ty: {}, doc: String::new(), content: {} }}",
50        field.name,
51        type_expr(&field.ty),
52        field.content
53    )
54}
55
56fn fields_expr(fields: &[FieldDef]) -> String {
57    fields.iter().map(field_expr).collect::<Vec<_>>().join(", ")
58}
59
60fn enum_expr(def: &EnumDef) -> String {
61    let variants: Vec<_> = def
62        .variants
63        .iter()
64        .map(|variant| {
65            // A label keeps its key and the fields it shows; its words
66            // come from the string tables.
67            let label = match &variant.label {
68                Some(label) => format!(
69                    "Some({P}::LocalizedText {{ key: {:?}.into(), translations: ::std::default::Default::default(), params: vec![{}] }})",
70                    label.key,
71                    label
72                        .params
73                        .iter()
74                        .map(|param| format!("{param:?}.into()"))
75                        .collect::<Vec<_>>()
76                        .join(", ")
77                ),
78                None => "None".into(),
79            };
80            format!(
81                "{P}::VariantDef {{ name: {:?}.into(), fields: vec![{}], label: {label} }}",
82                variant.name,
83                fields_expr(&variant.fields)
84            )
85        })
86        .collect();
87    format!(
88        "{P}::EnumDef {{ name: {:?}.into(), variants: vec![{}] }}",
89        def.name,
90        variants.join(", ")
91    )
92}
93
94fn record_expr(def: &RecordDef) -> String {
95    format!(
96        "{P}::RecordDef {{ name: {:?}.into(), fields: vec![{}] }}",
97        def.name,
98        fields_expr(&def.fields)
99    )
100}
101
102pub fn types_expr(types: &TypeTable) -> String {
103    format!(
104        "{P}::TypeTable {{ enums: vec![{}], records: vec![{}] }}",
105        types
106            .enums
107            .iter()
108            .map(enum_expr)
109            .collect::<Vec<_>>()
110            .join(", "),
111        types
112            .records
113            .iter()
114            .map(record_expr)
115            .collect::<Vec<_>>()
116            .join(", ")
117    )
118}
119
120/// How the target presents every window kind, as a `Presentation` variant.
121#[derive(Clone, Copy)]
122pub enum WindowPresentation {
123    Native,
124    Virtual,
125}
126
127/// The `bern_ffi::runtime::SessionSpec` of an application.
128pub fn session_spec(app: &Application, presentation: WindowPresentation) -> String {
129    let presentation = match presentation {
130        WindowPresentation::Native => "bern_ffi::runtime::Presentation::Native",
131        WindowPresentation::Virtual => "bern_ffi::runtime::Presentation::Virtual",
132    };
133    session_spec_with(app, &|_| presentation.to_string())
134}
135
136/// The `bern_ffi::runtime::SessionSpec` of an application whose window
137/// kinds present as the Rust expression `presentation` returns for each,
138/// for targets that choose at run time.
139pub fn session_spec_with(
140    app: &Application,
141    presentation: &dyn Fn(&bern_ir::WindowDef) -> String,
142) -> String {
143    let sessions: Vec<String> = app
144        .sessions
145        .iter()
146        .map(|session| {
147            let fields: Vec<String> = session
148                .fields
149                .iter()
150                .map(|field| {
151                    format!(
152                        "({:?}.into(), {}, {})",
153                        field.name,
154                        type_expr(&field.ty),
155                        value_expr(&field.initial)
156                    )
157                })
158                .collect();
159            format!(
160                "bern_ffi::SessionType {{ name: {:?}.into(), fields: vec![{}] }}",
161                session.name,
162                fields.join(", ")
163            )
164        })
165        .collect();
166    let windows: Vec<String> = app
167        .windows
168        .windows
169        .iter()
170        .map(|window| {
171            let indices: Vec<String> = window
172                .sessions
173                .iter()
174                .filter_map(|name| {
175                    app.sessions
176                        .iter()
177                        .position(|session| session.name == *name)
178                })
179                .map(|index| index.to_string())
180                .collect();
181            format!(
182                "bern_ffi::WindowKind {{ id: {:?}.into(), sessions: vec![{}], presentation: {} }}",
183                window.id,
184                indices.join(", "),
185                presentation(window)
186            )
187        })
188        .collect();
189    format!(
190        "bern_ffi::runtime::SessionSpec {{\n            types: {},\n            sessions: vec![\n                {}\n            ],\n            windows: vec![\n                {}\n            ],\n        }}",
191        types_expr(&app.types),
192        sessions.join(",\n                "),
193        windows.join(",\n                ")
194    )
195}
196
197/// `vec![…]` items for the application's players and declared resources.
198pub fn players_expr(app: &Application) -> String {
199    app.players
200        .iter()
201        .map(|player| format!("{:?}.into()", player.id))
202        .collect::<Vec<_>>()
203        .join(", ")
204}
205
206pub fn resources_expr(app: &Application) -> String {
207    app.resources
208        .iter()
209        .map(|resource| format!("{:?}.into()", resource.name))
210        .collect::<Vec<_>>()
211        .join(", ")
212}
213
214/// The expression creating the application engine: the protocol adapter
215/// around the engine, or with modules the composite of the shell and every
216/// module engine.
217pub fn engine_expr(context: &crate::BuildContext) -> String {
218    let adapter = |protocol: &str, engine: &str| {
219        format!("{protocol}::Adapter(<{engine} as ::std::default::Default>::default())")
220    };
221    let shell = adapter(&context.protocol_module, &context.engine_type);
222    if context.modules.is_empty() {
223        return shell;
224    }
225    let modules: Vec<String> = context
226        .modules
227        .iter()
228        .map(|module| {
229            let depends: Vec<String> = module
230                .depends
231                .iter()
232                .map(|id| format!("{id:?}.to_string()"))
233                .collect();
234            format!(
235                "::bern::__private::command::ModuleEngine {{ descriptor: {}::descriptor(), start: ::std::boxed::Box::new(|| ::std::boxed::Box::new({})), optional: {}, enabled: {}, depends: ::std::vec![{}] }}",
236                module.protocol_module,
237                adapter(&module.protocol_module, &module.engine_type),
238                module.optional,
239                module.enabled,
240                depends.join(", ")
241            )
242        })
243        .collect();
244    format!(
245        "::bern::__private::command::Composite::new(::std::boxed::Box::new({shell}), ::std::vec![{}]).unwrap_or_else(|error| panic!(\"{{error}}\"))",
246        modules.join(", ")
247    )
248}
249
250/// The expression giving the application catalog as a
251/// `&'static ProtocolDescriptor`.
252pub fn descriptor_expr(context: &crate::BuildContext) -> String {
253    let shell = format!("{}::descriptor()", context.protocol_module);
254    if context.modules.is_empty() {
255        return shell;
256    }
257    let modules: Vec<String> = context
258        .modules
259        .iter()
260        .map(|module| {
261            format!(
262                "::bern::__private::protocol::ModulePart {{ descriptor: {}::descriptor(), optional: {} }}",
263                module.protocol_module, module.optional
264            )
265        })
266        .collect();
267    format!(
268        "::std::boxed::Box::leak(::std::boxed::Box::new(::bern::__private::protocol::compose({shell}, &[{}]).unwrap_or_else(|error| panic!(\"{{error}}\"))))",
269        modules.join(", ")
270    )
271}
272
273/// Cargo dependency lines for the modules' protocol crates, their engine
274/// crates when `engines` is set, and `bern` itself, which composes them.
275pub fn module_dependencies(context: &crate::BuildContext, engines: bool) -> String {
276    if context.modules.is_empty() {
277        return String::new();
278    }
279    let path = |path: &std::path::Path| path.display().to_string().replace('\\', "/");
280    let mut lines = vec![format!(
281        "bern = {{ {} }}",
282        context.framework.dependency("bern")
283    )];
284    for module in &context.modules {
285        lines.push(format!(
286            "{} = {{ path = \"{}\" }}",
287            module.protocol_package,
288            path(&module.protocol_path)
289        ));
290        if engines {
291            lines.push(format!(
292                "{} = {{ path = \"{}\" }}",
293                module.engine_package,
294                path(&module.engine_path)
295            ));
296        }
297    }
298    lines.join("\n") + "\n"
299}
300
301/// A path as generated manifests and sources write it, with `/` between
302/// components on every system.
303pub fn path(path: &Path) -> String {
304    path.display().to_string().replace('\\', "/")
305}
306
307/// What the `[profile.release]` of every generated crate sets: link-time
308/// optimization across all crates in one code generation unit, and
309/// programs without a symbol table. Packages ship release builds, and
310/// this keeps the command line and the MCP server beside an app small;
311/// debug builds keep Cargo's defaults.
312pub const RELEASE_PROFILE: &str = "codegen-units = 1\nlto = true\nstrip = true\n";
313
314/// The `[profile]` tables of a generated crate. A generated crate is a
315/// workspace of its own, so the application's profiles would not reach the
316/// programs a target builds: they are carried over from its workspace
317/// manifest. Release builds start from `RELEASE_PROFILE`, take the
318/// application's own release settings over it, and keep `fixed`, the lines
319/// the target cannot do without (unwinding across the FFI, say); the other
320/// profiles, such as optimized libraries in debug builds, are the
321/// application's as they are.
322pub fn profiles(context: &crate::BuildContext, fixed: &str) -> String {
323    let table = |text: &str| text.parse::<toml::Table>().unwrap_or_default();
324    let mut release = table(RELEASE_PROFILE);
325    let mut tables = toml::Table::new();
326    let own = std::fs::read_to_string(context.workspace_dir.join("Cargo.toml"))
327        .ok()
328        .and_then(|text| text.parse::<toml::Table>().ok())
329        .and_then(|manifest| manifest.get("profile")?.as_table().cloned())
330        .unwrap_or_default();
331    for (name, value) in own {
332        match (name.as_str(), value) {
333            ("release", toml::Value::Table(settings)) => release.extend(settings),
334            (_, value) => {
335                tables.insert(name, value);
336            }
337        }
338    }
339    release.extend(table(fixed));
340    tables.insert("release".into(), toml::Value::Table(release));
341    let mut root = toml::Table::new();
342    root.insert("profile".into(), toml::Value::Table(tables));
343    toml::to_string(&root).unwrap_or_default()
344}
345
346/// Name stem of the generated programs, for example `showcase`.
347pub fn program(app: &Application) -> String {
348    app.app.id.replace('_', "-")
349}
350
351/// `main.rs` of the application CLI, derived from the command catalog.
352pub fn cli_rs(context: &crate::BuildContext, app: &Application) -> String {
353    format!(
354        r#"//! Generated by Bern. Application CLI derived from the command catalog.
355
356fn main() {{
357    std::process::exit(bern_adapter_cli::run(
358        {descriptor},
359        {bundle:?},
360        {name:?},
361    ));
362}}
363"#,
364        descriptor = descriptor_expr(context),
365        bundle = app.app.bundle_id,
366        name = program(app),
367    )
368}
369
370/// `main.rs` of the MCP server, derived from the command catalog.
371pub fn mcp_rs(context: &crate::BuildContext, app: &Application) -> String {
372    format!(
373        r#"//! Generated by Bern. MCP server derived from the command catalog.
374
375fn main() {{
376    std::process::exit(bern_adapter_mcp::run(
377        {descriptor},
378        {bundle:?},
379        {name:?},
380        {version:?},
381    ));
382}}
383"#,
384        descriptor = descriptor_expr(context),
385        bundle = app.app.bundle_id,
386        name = program(app),
387        version = app.app.version,
388    )
389}