wry_bindgen/
function_registry.rs

1//! Function registry for collecting and managing JS functions and exports.
2//!
3//! This module provides the registry system that collects JS function specifications,
4//! inline JS modules, and exported Rust types via the `inventory` crate.
5
6use alloc::collections::BTreeMap;
7use alloc::string::{String, ToString};
8use alloc::vec::Vec;
9use core::fmt::Write;
10use core::ops::Deref;
11use once_cell::sync::{Lazy, OnceCell};
12
13use crate::function::JSFunction;
14use crate::ipc::{DecodedData, EncodedData};
15
16/// Function specification for the registry
17#[derive(Clone, Copy)]
18pub struct JsFunctionSpec {
19    /// Function that generates the JS code
20    js_code: fn() -> String,
21}
22
23impl JsFunctionSpec {
24    pub const fn new(js_code: fn() -> String) -> Self {
25        Self { js_code }
26    }
27
28    /// Get the JS code generator function
29    pub const fn js_code(&self) -> fn() -> String {
30        self.js_code
31    }
32
33    pub const fn resolve_as<F>(&self) -> LazyJsFunction<F> {
34        LazyJsFunction {
35            spec: *self,
36            inner: OnceCell::new(),
37        }
38    }
39}
40
41inventory::collect!(JsFunctionSpec);
42
43/// A type that dynamically resolves to a JSFunction from the registry on first use.
44pub struct LazyJsFunction<F> {
45    spec: JsFunctionSpec,
46    inner: OnceCell<JSFunction<F>>,
47}
48
49impl<F> Deref for LazyJsFunction<F> {
50    type Target = JSFunction<F>;
51
52    fn deref(&self) -> &Self::Target {
53        self.inner.get_or_init(|| {
54            FUNCTION_REGISTRY
55                .get_function(self.spec)
56                .unwrap_or_else(|| {
57                    panic!("Function not found for code: {}", (self.spec.js_code())())
58                })
59        })
60    }
61}
62
63/// Inline JS module info
64#[derive(Clone, Copy)]
65pub struct InlineJsModule {
66    /// The JS module content
67    content: &'static str,
68}
69
70impl InlineJsModule {
71    pub const fn new(content: &'static str) -> Self {
72        Self { content }
73    }
74
75    /// Get the JS module content
76    pub const fn content(&self) -> &'static str {
77        self.content
78    }
79
80    /// Calculate the hash of the module content for use as a filename
81    /// This uses a simple FNV-1a hash that can also be computed at compile time
82    pub fn hash(&self) -> String {
83        format!("{:x}", self.const_hash())
84    }
85
86    /// Const-compatible hash function (FNV-1a)
87    pub const fn const_hash(&self) -> u64 {
88        const FNV_OFFSET_BASIS: u64 = 0xcbf29ce484222325;
89        const FNV_PRIME: u64 = 0x100000001b3;
90
91        let mut hash = FNV_OFFSET_BASIS;
92        let mut i = 0;
93        let bytes = self.content.as_bytes();
94        while i < bytes.len() {
95            hash ^= bytes[i] as u64;
96            hash = hash.wrapping_mul(FNV_PRIME);
97            i += 1;
98        }
99        hash
100    }
101}
102
103inventory::collect!(InlineJsModule);
104
105/// Type of class member for exported Rust structs
106#[derive(Clone, Copy, Debug, PartialEq, Eq)]
107pub enum JsClassMemberKind {
108    /// Constructor function (e.g., `Counter.new`)
109    Constructor,
110    /// Instance method on prototype (e.g., `Counter.prototype.increment`)
111    Method,
112    /// Static method on class (e.g., `Counter.staticMethod`)
113    StaticMethod,
114    /// Property getter (e.g., `get count()`)
115    Getter,
116    /// Property setter (e.g., `set count(v)`)
117    Setter,
118}
119
120/// Specification for a member of an exported Rust class
121///
122/// All class members (methods, constructors, getters, setters) are collected
123/// and used to generate complete class code in FunctionRegistry.
124#[derive(Clone, Copy)]
125pub struct JsClassMemberSpec {
126    /// The class name this member belongs to (e.g., "Counter")
127    class_name: &'static str,
128    /// The JavaScript member name (e.g., "increment", "count")
129    member_name: &'static str,
130    /// The export name for IPC calls (e.g., "Counter::increment")
131    export_name: &'static str,
132    /// Number of arguments (excluding self/handle)
133    arg_count: usize,
134    /// Type of member
135    kind: JsClassMemberKind,
136}
137
138impl JsClassMemberSpec {
139    pub const fn new(
140        class_name: &'static str,
141        member_name: &'static str,
142        export_name: &'static str,
143        arg_count: usize,
144        kind: JsClassMemberKind,
145    ) -> Self {
146        Self {
147            class_name,
148            member_name,
149            export_name,
150            arg_count,
151            kind,
152        }
153    }
154
155    /// Get the class name this member belongs to
156    pub const fn class_name(&self) -> &'static str {
157        self.class_name
158    }
159
160    /// Get the JavaScript member name
161    pub const fn member_name(&self) -> &'static str {
162        self.member_name
163    }
164
165    /// Get the export name for IPC calls
166    pub const fn export_name(&self) -> &'static str {
167        self.export_name
168    }
169
170    /// Get the number of arguments (excluding self/handle)
171    pub const fn arg_count(&self) -> usize {
172        self.arg_count
173    }
174
175    /// Get the type of member
176    pub const fn kind(&self) -> JsClassMemberKind {
177        self.kind
178    }
179}
180
181inventory::collect!(JsClassMemberSpec);
182
183/// Specification for an exported Rust function/method callable from JavaScript.
184///
185/// This is used by the `#[wasm_bindgen]` macro when exporting structs and impl blocks.
186/// Each export is registered via inventory and collected at runtime.
187#[derive(Clone, Copy)]
188pub struct JsExportSpec {
189    /// The export name (e.g., "MyStruct::new", "MyStruct::method")
190    pub name: &'static str,
191    /// Handler function that decodes arguments, calls the Rust function, and encodes the result
192    pub handler: fn(&mut DecodedData) -> Result<EncodedData, alloc::string::String>,
193}
194
195impl JsExportSpec {
196    pub const fn new(
197        name: &'static str,
198        handler: fn(&mut DecodedData) -> Result<EncodedData, alloc::string::String>,
199    ) -> Self {
200        Self { name, handler }
201    }
202}
203
204inventory::collect!(JsExportSpec);
205
206/// Registry of JS functions collected via inventory
207pub(crate) struct FunctionRegistry {
208    functions: String,
209    function_specs: Vec<JsFunctionSpec>,
210    /// Map of module path -> module content for inline_js modules
211    modules: BTreeMap<String, &'static str>,
212}
213
214/// The registry of javascript functions registered via inventory. This
215/// is shared between all webviews.
216pub(crate) static FUNCTION_REGISTRY: Lazy<FunctionRegistry> =
217    Lazy::new(FunctionRegistry::collect_from_inventory);
218
219/// Generate argument names for JS function (a0, a1, a2, ...)
220fn generate_args(count: usize) -> String {
221    (0..count)
222        .map(|i| format!("a{i}"))
223        .collect::<Vec<_>>()
224        .join(", ")
225}
226
227impl FunctionRegistry {
228    fn collect_from_inventory() -> Self {
229        let mut modules = BTreeMap::new();
230
231        // Collect all inline JS modules and deduplicate by content hash
232        for inline_js in inventory::iter::<InlineJsModule>() {
233            let hash = inline_js.hash();
234            let module_path = format!("{hash}.js");
235            // Only insert if we haven't seen this content before
236            modules.entry(module_path).or_insert(inline_js.content());
237        }
238
239        // Collect all function specs
240        let specs: Vec<_> = inventory::iter::<JsFunctionSpec>().copied().collect();
241
242        // Build the script - load modules from wry:// handler before setting up function registry
243        let mut script = String::new();
244
245        // Wrap everything in an async IIFE to use await
246        script.push_str("(async () => {\n");
247
248        // Track which modules we've already imported (by hash)
249        let mut imported_modules = alloc::collections::BTreeSet::new();
250
251        // Load all inline_js modules from the wry handler (deduplicated by content hash)
252        for inline_js in inventory::iter::<InlineJsModule>() {
253            let hash = inline_js.hash();
254            // Only import each unique module once
255            if imported_modules.insert(hash.clone()) {
256                // Dynamically import the module from /__wbg__/snippets/{hash}.js
257                writeln!(
258                    &mut script,
259                    "  const module_{hash} = await import('/__wbg__/snippets/{hash}.js');"
260                )
261                .unwrap();
262            }
263        }
264
265        // Now set up the function registry after all modules are loaded
266        // Store raw JS functions - type info will be passed at call time
267        script.push_str("  window.setFunctionRegistry([");
268        for (i, spec) in specs.iter().enumerate() {
269            if i > 0 {
270                script.push_str(",\n");
271            }
272            let js_code = (spec.js_code())();
273            write!(&mut script, "{js_code}").unwrap();
274        }
275        script.push_str("]);\n");
276
277        // Collect all class members and group by class name
278        let mut class_members: BTreeMap<&str, Vec<&JsClassMemberSpec>> = BTreeMap::new();
279        for member in inventory::iter::<JsClassMemberSpec>() {
280            class_members
281                .entry(member.class_name())
282                .or_default()
283                .push(member);
284        }
285
286        // Generate complete class definitions for each exported struct
287        for (class_name, members) in &class_members {
288            // Generate class shell
289            writeln!(
290                &mut script,
291                r#"  class {class_name} {{
292    constructor(handle) {{
293      this.__handle = handle;
294      this.__className = "{class_name}";
295      window.__wryExportRegistry.register(this, {{ handle, className: "{class_name}" }});
296    }}
297    static __wrap(handle) {{
298      const obj = Object.create({class_name}.prototype);
299      obj.__handle = handle;
300      obj.__className = "{class_name}";
301      window.__wryExportRegistry.register(obj, {{ handle, className: "{class_name}" }});
302      return obj;
303    }}
304    free() {{
305      const handle = this.__handle;
306      this.__handle = 0;
307      if (handle !== 0) window.__wryCallExport("{class_name}::__drop", handle);
308    }}"#
309            )
310            .unwrap();
311
312            // Track getters/setters to combine them into single property descriptors
313            let mut getters: BTreeMap<&str, &JsClassMemberSpec> = BTreeMap::new();
314            let mut setters: BTreeMap<&str, &JsClassMemberSpec> = BTreeMap::new();
315
316            // Generate methods inside the class body
317            for member in members {
318                match member.kind() {
319                    JsClassMemberKind::Method => {
320                        // Instance method
321                        let args = generate_args(member.arg_count());
322                        let args_with_handle = if member.arg_count() > 0 {
323                            format!("this.__handle, {args}")
324                        } else {
325                            "this.__handle".to_string()
326                        };
327                        writeln!(
328                            &mut script,
329                            r#"    {}({}) {{ return window.__wryCallExport("{}", {}); }}"#,
330                            member.member_name(),
331                            args,
332                            member.export_name(),
333                            args_with_handle
334                        )
335                        .unwrap();
336                    }
337                    JsClassMemberKind::Getter => {
338                        getters.insert(member.member_name(), member);
339                    }
340                    JsClassMemberKind::Setter => {
341                        setters.insert(member.member_name(), member);
342                    }
343                    _ => {} // Constructor and static handled separately
344                }
345            }
346
347            // Generate getters/setters as property accessors inside the class
348            let mut property_names: alloc::collections::BTreeSet<&str> =
349                alloc::collections::BTreeSet::new();
350            property_names.extend(getters.keys());
351            property_names.extend(setters.keys());
352
353            for prop_name in property_names {
354                let getter = getters.get(prop_name);
355                let setter = setters.get(prop_name);
356                match (getter, setter) {
357                    (Some(g), Some(s)) => {
358                        writeln!(
359                            &mut script,
360                            r#"    get {}() {{ return window.__wryCallExport("{}", this.__handle); }}
361    set {}(v) {{ window.__wryCallExport("{}", this.__handle, v); }}"#,
362                            prop_name, g.export_name(), prop_name, s.export_name()
363                        )
364                        .unwrap();
365                    }
366                    (Some(g), None) => {
367                        writeln!(
368                            &mut script,
369                            r#"    get {}() {{ return window.__wryCallExport("{}", this.__handle); }}"#,
370                            prop_name, g.export_name()
371                        )
372                        .unwrap();
373                    }
374                    (None, Some(s)) => {
375                        writeln!(
376                            &mut script,
377                            r#"    set {}(v) {{ window.__wryCallExport("{}", this.__handle, v); }}"#,
378                            prop_name, s.export_name()
379                        )
380                        .unwrap();
381                    }
382                    (None, None) => {}
383                }
384            }
385
386            // Close the class body
387            script.push_str("  }\n");
388
389            // Add static methods and constructors outside the class
390            for member in members {
391                match member.kind() {
392                    JsClassMemberKind::Constructor => {
393                        let args = generate_args(member.arg_count());
394                        let args_call = if member.arg_count() > 0 { &args } else { "" };
395                        writeln!(
396                            &mut script,
397                            r#"  {class_name}.{method_name} = function({args}) {{ const handle = window.__wryCallExport("{export_name}", {args_call}); return {class_name}.__wrap(handle); }};"#,
398                            class_name = class_name,
399                            method_name = member.member_name(),
400                            args = args,
401                            export_name = member.export_name(),
402                            args_call = args_call
403                        )
404                        .unwrap();
405                    }
406                    JsClassMemberKind::StaticMethod => {
407                        let args = generate_args(member.arg_count());
408                        let args_call = if member.arg_count() > 0 { &args } else { "" };
409                        writeln!(
410                            &mut script,
411                            r#"  {class_name}.{method_name} = function({args}) {{ return window.__wryCallExport("{export_name}", {args_call}); }};"#,
412                            class_name = class_name,
413                            method_name = member.member_name(),
414                            args = args,
415                            export_name = member.export_name(),
416                            args_call = args_call
417                        )
418                        .unwrap();
419                    }
420                    _ => {} // Methods, getters, setters already handled
421                }
422            }
423
424            // Register class on window
425            writeln!(&mut script, "  window.{class_name} = {class_name};").unwrap();
426        }
427
428        // Send a request to wry to notify that the function registry is initialized
429        script.push_str("  fetch(`/__wbg__/initialized`, { method: 'POST', body: [] });\n");
430
431        // Close the async IIFE
432        script.push_str("})();\n");
433
434        Self {
435            functions: script,
436            function_specs: specs,
437            modules,
438        }
439    }
440
441    /// Get a function by name from the registry
442    pub fn get_function<F>(&self, spec: JsFunctionSpec) -> Option<JSFunction<F>> {
443        let index = self
444            .function_specs
445            .iter()
446            .position(|s| s.js_code() as usize == spec.js_code() as usize)?;
447        Some(JSFunction::new(index as _))
448    }
449
450    /// Get the initialization script
451    pub fn script(&self) -> &str {
452        &self.functions
453    }
454
455    /// Get the content of an inline_js module by path
456    pub fn get_module(&self, path: &str) -> Option<&'static str> {
457        self.modules.get(path).copied()
458    }
459}