1use 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#[derive(Clone, Copy)]
18pub struct JsFunctionSpec {
19 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 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
43pub 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#[derive(Clone, Copy)]
65pub struct InlineJsModule {
66 content: &'static str,
68}
69
70impl InlineJsModule {
71 pub const fn new(content: &'static str) -> Self {
72 Self { content }
73 }
74
75 pub const fn content(&self) -> &'static str {
77 self.content
78 }
79
80 pub fn hash(&self) -> String {
83 format!("{:x}", self.const_hash())
84 }
85
86 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#[derive(Clone, Copy, Debug, PartialEq, Eq)]
107pub enum JsClassMemberKind {
108 Constructor,
110 Method,
112 StaticMethod,
114 Getter,
116 Setter,
118}
119
120#[derive(Clone, Copy)]
125pub struct JsClassMemberSpec {
126 class_name: &'static str,
128 member_name: &'static str,
130 export_name: &'static str,
132 arg_count: usize,
134 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 pub const fn class_name(&self) -> &'static str {
157 self.class_name
158 }
159
160 pub const fn member_name(&self) -> &'static str {
162 self.member_name
163 }
164
165 pub const fn export_name(&self) -> &'static str {
167 self.export_name
168 }
169
170 pub const fn arg_count(&self) -> usize {
172 self.arg_count
173 }
174
175 pub const fn kind(&self) -> JsClassMemberKind {
177 self.kind
178 }
179}
180
181inventory::collect!(JsClassMemberSpec);
182
183#[derive(Clone, Copy)]
188pub struct JsExportSpec {
189 pub name: &'static str,
191 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
206pub(crate) struct FunctionRegistry {
208 functions: String,
209 function_specs: Vec<JsFunctionSpec>,
210 modules: BTreeMap<String, &'static str>,
212}
213
214pub(crate) static FUNCTION_REGISTRY: Lazy<FunctionRegistry> =
217 Lazy::new(FunctionRegistry::collect_from_inventory);
218
219fn 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 for inline_js in inventory::iter::<InlineJsModule>() {
233 let hash = inline_js.hash();
234 let module_path = format!("{hash}.js");
235 modules.entry(module_path).or_insert(inline_js.content());
237 }
238
239 let specs: Vec<_> = inventory::iter::<JsFunctionSpec>().copied().collect();
241
242 let mut script = String::new();
244
245 script.push_str("(async () => {\n");
247
248 let mut imported_modules = alloc::collections::BTreeSet::new();
250
251 for inline_js in inventory::iter::<InlineJsModule>() {
253 let hash = inline_js.hash();
254 if imported_modules.insert(hash.clone()) {
256 writeln!(
258 &mut script,
259 " const module_{hash} = await import('/__wbg__/snippets/{hash}.js');"
260 )
261 .unwrap();
262 }
263 }
264
265 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 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 for (class_name, members) in &class_members {
288 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 let mut getters: BTreeMap<&str, &JsClassMemberSpec> = BTreeMap::new();
314 let mut setters: BTreeMap<&str, &JsClassMemberSpec> = BTreeMap::new();
315
316 for member in members {
318 match member.kind() {
319 JsClassMemberKind::Method => {
320 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 _ => {} }
345 }
346
347 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 script.push_str(" }\n");
388
389 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 _ => {} }
422 }
423
424 writeln!(&mut script, " window.{class_name} = {class_name};").unwrap();
426 }
427
428 script.push_str(" fetch(`/__wbg__/initialized`, { method: 'POST', body: [] });\n");
430
431 script.push_str("})();\n");
433
434 Self {
435 functions: script,
436 function_specs: specs,
437 modules,
438 }
439 }
440
441 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 pub fn script(&self) -> &str {
452 &self.functions
453 }
454
455 pub fn get_module(&self, path: &str) -> Option<&'static str> {
457 self.modules.get(path).copied()
458 }
459}