Skip to main content

gdscript_api/
lib.rs

1//! `gdscript-api` — the Godot engine model, generated from `extension_api.json`.
2//!
3//! The model (engine classes + inheritance chain, methods, properties, signals, enums,
4//! constants, singletons, utility functions, builtin Variant types) plus the hand-authored
5//! GDScript layer the dump omits (pseudo-constants + builtin functions). See
6//! `plans/PHASE-2-IMPLEMENTATION-PLAYBOOK.md` §4.
7//!
8//! ## Shape
9//! [`model::ApiData`] is the serializable root that `xtask codegen-api` `rkyv`-encodes into a
10//! binary blob; [`EngineApi`] deserializes it once, rebuilds the name indices, merges the
11//! hand-authored layer, and exposes the lookup API (`lookup.rs`). The model is `Arc`-shared
12//! and excluded from per-file timing, so the one-time deserialize is amortized.
13//!
14//! ## Targets
15//! Native builds embed the blob via `include_bytes!` ([`bundled`], behind the default
16//! `bundled-api` feature). The crate never touches `std::fs`/clocks/threads, so it builds for
17//! `wasm32`; there the blob is **not** embedded (Playbook §4.5) — the host fetches it and calls
18//! [`EngineApi::from_bytes`].
19#![cfg_attr(docsrs, feature(doc_cfg))]
20
21pub mod gdscript_layer;
22/// Generated engine-API metadata (version + counts). Produced by `cargo xtask codegen-api`.
23pub mod generated;
24pub mod lookup;
25pub mod model;
26
27use rustc_hash::FxHashMap;
28
29pub use lookup::MemberRef;
30pub use model::{
31    ApiData, ApiType, ApiVersion, BuiltinData, BuiltinId, BuiltinMember, ClassData, ClassId,
32    ConstInfo, DocId, ElemRef, EnumInfo, EnumValue, MethodSig, OperatorSig, Param, PropertyInfo,
33    SignalSig, TyRef, UtilityFn,
34};
35
36/// The Godot version string the bundled engine-API artifact was generated from.
37#[must_use]
38pub fn godot_version() -> &'static str {
39    generated::GODOT_VERSION
40}
41
42/// An error loading the engine-API blob.
43#[derive(Debug, Clone, PartialEq, Eq)]
44pub enum LoadError {
45    /// The `rkyv` blob failed to validate or decode.
46    Decode(String),
47}
48
49impl std::fmt::Display for LoadError {
50    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
51        match self {
52            Self::Decode(msg) => write!(f, "failed to decode engine-API blob: {msg}"),
53        }
54    }
55}
56
57impl std::error::Error for LoadError {}
58
59/// The loaded, indexed Godot engine model.
60///
61/// Holds the deserialized [`ApiData`] plus the name → id indices rebuilt at load (kept out of
62/// the blob so the archived form stays portable — Playbook §4.5) and the hand-authored
63/// GDScript layer (pseudo-constants + builtin functions).
64#[derive(Debug)]
65pub struct EngineApi {
66    pub(crate) data: ApiData,
67    pub(crate) class_by_name: FxHashMap<String, ClassId>,
68    pub(crate) builtin_by_name: FxHashMap<String, BuiltinId>,
69    pub(crate) singleton_by_name: FxHashMap<String, ClassId>,
70    pub(crate) utility_by_name: FxHashMap<String, u32>,
71    pub(crate) global_enum_by_name: FxHashMap<String, u32>,
72    /// Hand-authored `@GlobalScope`/`@GDScript` pseudo-constants (`PI`/`TAU`/`INF`/`NAN`).
73    pub(crate) global_consts: Vec<gdscript_layer::GlobalConst>,
74    /// Hand-authored GDScript builtin functions (`preload`/`range`/`len`/…).
75    pub(crate) gdscript_builtins: Vec<gdscript_layer::BuiltinFn>,
76    /// Cached id of the `int` builtin (used to type engine-class integer constants).
77    pub(crate) int_builtin: Option<BuiltinId>,
78}
79
80impl EngineApi {
81    /// Build the indexed model from a freshly decoded [`ApiData`], rebuilding the name indices
82    /// and merging the hand-authored GDScript layer.
83    #[must_use]
84    pub fn from_data(data: ApiData) -> Self {
85        let mut class_by_name = FxHashMap::default();
86        for (i, c) in data.classes.iter().enumerate() {
87            class_by_name.insert(
88                c.name.clone(),
89                ClassId(u32::try_from(i).unwrap_or(u32::MAX)),
90            );
91        }
92        let mut builtin_by_name = FxHashMap::default();
93        for (i, b) in data.builtins.iter().enumerate() {
94            builtin_by_name.insert(
95                b.name.clone(),
96                BuiltinId(u32::try_from(i).unwrap_or(u32::MAX)),
97            );
98        }
99        let singleton_by_name = data
100            .singletons
101            .iter()
102            .map(|(name, id)| (name.clone(), *id))
103            .collect();
104        let mut utility_by_name = FxHashMap::default();
105        for (i, u) in data.utilities.iter().enumerate() {
106            utility_by_name.insert(u.name.clone(), u32::try_from(i).unwrap_or(u32::MAX));
107        }
108        let mut global_enum_by_name = FxHashMap::default();
109        for (i, e) in data.global_enums.iter().enumerate() {
110            global_enum_by_name.insert(e.name.clone(), u32::try_from(i).unwrap_or(u32::MAX));
111        }
112        let int_builtin = builtin_by_name.get("int").copied();
113
114        Self {
115            data,
116            class_by_name,
117            builtin_by_name,
118            singleton_by_name,
119            utility_by_name,
120            global_enum_by_name,
121            global_consts: gdscript_layer::global_consts(),
122            gdscript_builtins: gdscript_layer::builtin_fns(),
123            int_builtin,
124        }
125    }
126
127    /// Decode and index an engine-API blob produced by `xtask codegen-api`.
128    ///
129    /// The bytes are copied into a 16-byte-aligned buffer before validation so a misaligned
130    /// source (e.g. `include_bytes!` or a `fetch()`ed `ArrayBuffer`) decodes correctly.
131    ///
132    /// # Errors
133    /// Returns [`LoadError::Decode`] if the blob fails `rkyv` validation.
134    pub fn from_bytes(bytes: &[u8]) -> Result<Self, LoadError> {
135        let mut aligned = rkyv::util::AlignedVec::<16>::new();
136        aligned.extend_from_slice(bytes);
137        let data = rkyv::from_bytes::<ApiData, rkyv::rancor::Error>(aligned.as_slice())
138            .map_err(|e| LoadError::Decode(e.to_string()))?;
139        Ok(Self::from_data(data))
140    }
141
142    /// The Godot version this model was generated from.
143    #[must_use]
144    pub fn version(&self) -> &ApiVersion {
145        &self.data.version
146    }
147}
148
149/// The bundled engine-API model, decoded once on first use.
150///
151/// Native-only and gated on the default `bundled-api` feature: the blob is embedded via
152/// `include_bytes!`. On `wasm32` the blob is not embedded — fetch it and use
153/// [`EngineApi::from_bytes`] instead (Playbook §4.5).
154///
155/// # Panics
156/// Panics if the embedded blob fails to decode, which can only happen if `engine_api.bin` was
157/// hand-edited or truncated — `cargo xtask codegen-api` always emits a valid, self-validated
158/// artifact.
159#[cfg(all(feature = "bundled-api", not(target_arch = "wasm32")))]
160#[must_use]
161pub fn bundled() -> &'static EngineApi {
162    use std::sync::OnceLock;
163    static BUNDLED: OnceLock<EngineApi> = OnceLock::new();
164    static BYTES: &[u8] = include_bytes!("engine_api.bin");
165    BUNDLED.get_or_init(|| {
166        EngineApi::from_bytes(BYTES).expect("the bundled engine-API blob must be valid")
167    })
168}
169
170#[cfg(test)]
171mod tests {
172    #[test]
173    fn generated_metadata_is_present() {
174        // Regenerated by `cargo xtask codegen-api`; the version string is always populated.
175        assert!(!crate::generated::GODOT_VERSION.is_empty());
176    }
177
178    // The bundled blob is native-only behind the default feature (see `bundled`).
179    #[cfg(all(feature = "bundled-api", not(target_arch = "wasm32")))]
180    #[test]
181    fn bundled_blob_loads_and_resolves_golden_symbols() {
182        let api = crate::bundled();
183
184        // Version came through the blob, not just `generated.rs`.
185        assert_eq!(api.version().major, 4);
186        assert_eq!(api.version().minor, 5);
187
188        // Direct + inherited member resolution and the inheritance walk.
189        let node = api.class_by_name("Node").expect("Node class present");
190        let node2d = api.class_by_name("Node2D").expect("Node2D class present");
191        assert!(api.lookup_member(node, "add_child").is_some());
192        assert!(api.is_subclass(node2d, node), "Node2D is a Node");
193        assert!(
194            api.lookup_member(node2d, "add_child").is_some(),
195            "add_child is inherited onto Node2D"
196        );
197
198        // The `recv.<TAB>` candidate set includes inherited members, deduped.
199        let members = api.members_of(node2d);
200        assert!(members.iter().any(|m| m.name() == "add_child"));
201        assert!(members.iter().any(|m| m.name() == "position"));
202
203        // Singletons, builtins + operators, the enum-property getter cross-ref.
204        assert!(api.singleton("Input").is_some());
205        let v2 = api
206            .builtin_by_name("Vector2")
207            .expect("Vector2 builtin present");
208        assert!(api.builtin_member(v2, "x").is_some());
209        assert!(api.builtin_operators(v2).iter().any(|o| o.op == "+"));
210        let process_mode = api
211            .class(node)
212            .properties
213            .iter()
214            .find(|p| p.name == "process_mode")
215            .expect("Node.process_mode present");
216        assert!(
217            process_mode.enum_of.is_some(),
218            "process_mode is recovered as enum-typed from its getter"
219        );
220
221        // The hand-authored GDScript layer merged at load.
222        assert!(api.global_const("PI").is_some());
223        assert!(api.gdscript_builtin("preload").is_some());
224    }
225}