modrpc_codegen/codegen/wasm/
mod.rs

1use genco::prelude::*;
2use mproto_codegen::codegen::name_util::{
3    camel_to_kebab_case, camel_to_snake_case, snake_to_camel_case, snake_to_upper_camel_case,
4};
5
6use crate::{Database, ast::Interface};
7
8pub use project::wasm_project_gen;
9
10mod project;
11
12pub fn wit_interface(db: &Database, interface: &Interface, role_name: &str) -> String {
13    // Hook methods
14
15    let mut impl_notify_items = String::new();
16    let mut impl_items = String::new();
17    let mut hooks_notify_items = String::new();
18    let mut hooks_items = String::new();
19
20    for object in &interface.objects {
21        let Some(object_interface) = db.lookup_interface(&object.construct) else {
22            panic!("failed to lookup interface '{}'", object.construct.name);
23        };
24
25        let Some(object_role_name) = object.get_role_for_parent_role(object_interface, role_name)
26        else {
27            panic!(
28                "object '{}' failed to find role arg '{}'",
29                object.name, role_name
30            );
31        };
32
33        let object_name_wit = witify_snake_name(&object.name);
34
35        for required_impls_list in &object_interface.required_impls {
36            if !required_impls_list.roles.contains(object_role_name) {
37                continue;
38            }
39
40            for required_impl in &required_impls_list.required_impls {
41                let impl_name_wit = format!(
42                    "{}-{}",
43                    object_name_wit,
44                    witify_snake_name(&required_impl.name),
45                );
46                if required_impl.is_async {
47                    impl_items += &indoc::formatdoc! {"
48                        {impl_name_wit}: func(id: u32, input-buf: list<u8>)
49                    "};
50                    impl_notify_items += &indoc::formatdoc! {"
51                        notify-{impl_name_wit}: func(id: u32, output-buf: list<u8>)
52                    "};
53                } else {
54                    impl_items += &indoc::formatdoc! {"
55                        {impl_name_wit}: func(input-buf: list<u8>) -> list<u8>
56                    "};
57                }
58            }
59        }
60
61        for methods_list in &object_interface.methods {
62            if !methods_list.roles.contains(object_role_name) {
63                continue;
64            }
65
66            for method in &methods_list.methods {
67                let method_name_wit =
68                    format!("{}-{}", object_name_wit, witify_snake_name(&method.name),);
69                if method.is_async {
70                    hooks_items += &indoc::formatdoc! {"
71                        {method_name_wit}: func(input-buf: list<u8>) -> u32
72                    "};
73                    hooks_notify_items += &indoc::formatdoc! {"
74                        notify-{method_name_wit}: func(id: u32, output-buf: list<u8>)
75                    "};
76                } else {
77                    hooks_items += &indoc::formatdoc! {"
78                        {method_name_wit}: func(input-buf: list<u8>) -> list<u8>
79                    "};
80                }
81            }
82        }
83    }
84
85    let interface_role_name_wit = camel_to_kebab_case(&format!("{}{}", interface.name, role_name));
86
87    indoc::formatdoc! {"
88        default world {interface_role_name_wit} {{
89            import host: interface {{
90                modrpc-send-packet-bundle: func(buf: list<u8>)
91                {impl_items}
92                {hooks_notify_items}
93            }}
94
95            export guest: interface {{
96                {impl_notify_items}
97                {hooks_items}
98            }}
99
100            export main: interface {{
101                init: func(service-id: u32, endpoint-addr: u64, init-state-buf: list<u8>)
102                handle-packet: func(data: list<u8>)
103                tick: func()
104            }}
105        }}
106    "}
107}
108
109pub fn wit_impl_glue_rust(
110    interface_crate_name: &str,
111    db: &Database,
112    interface: &Interface,
113    role_name: &str,
114) -> rust::Tokens {
115    let mproto_cx = &mproto_codegen::codegen::CodegenCx::new_with_type_params(
116        db.mproto_db(),
117        Some(interface_crate_name),
118        false,
119        &interface.type_params,
120    );
121
122    let mut required_impl_notifies: rust::Tokens = quote! {};
123    let mut object_builds: rust::Tokens = quote! {};
124    let mut method_calls: rust::Tokens = quote! {};
125    for object in &interface.objects {
126        let Some(object_interface) = db.lookup_interface(&object.construct) else {
127            panic!("failed to lookup interface '{}'", object.construct.name);
128        };
129
130        let Some(object_role_name) = object.get_role_for_parent_role(object_interface, role_name)
131        else {
132            panic!(
133                "object '{}' failed to find role arg '{}'",
134                object.name, role_name
135            );
136        };
137
138        let object_mproto_cx =
139            &mproto_cx.with_type_args(&object_interface.type_params, &object.type_args);
140        let mut required_impl_handlers: rust::Tokens = quote! {};
141
142        for required_impls_list in &object_interface.required_impls {
143            if !required_impls_list.roles.contains(object_role_name) {
144                continue;
145            }
146
147            for required_impl in &required_impls_list.required_impls {
148                let input_type_tokens = mproto_codegen::codegen::rust::rust_type_tokens(
149                    object_mproto_cx,
150                    &required_impl.input_ty,
151                );
152                let output_type_tokens = mproto_codegen::codegen::rust::rust_type_tokens(
153                    object_mproto_cx,
154                    &required_impl.output_ty,
155                );
156
157                required_impl_notifies = quote! {
158                    $required_impl_notifies
159
160                    fn notify_$(&object.name)_$(&required_impl.name)(id: u32, result_buf: Vec<u8>) {
161                        modrpc_wasm_glue::notify_impl(id, result_buf)
162                    }
163                };
164
165                required_impl_handlers = quote! {
166                    $required_impl_handlers
167                    modrpc::async_handler!(
168                        { }
169                        |cx, requester, input: $input_type_tokens| -> $output_type_tokens {
170                            modrpc_wasm_glue::call_impl(host::$(&object.name)_$(&required_impl.name), input).await
171                        }
172                    ),
173                };
174            }
175        }
176
177        if object_interface.requires_impls_for_role(object_role_name) {
178            object_builds = quote! {
179                $object_builds
180
181                stubs.$(&object.name).build(
182                    s,
183                    $required_impl_handlers
184                );
185            };
186        }
187
188        for methods_list in &object_interface.methods {
189            if !methods_list.roles.contains(object_role_name) {
190                continue;
191            }
192
193            for method in &methods_list.methods {
194                let input_type_tokens = mproto_codegen::codegen::rust::rust_type_tokens(
195                    object_mproto_cx,
196                    &method.input_ty,
197                );
198                let output_type_tokens = mproto_codegen::codegen::rust::rust_type_tokens(
199                    object_mproto_cx,
200                    &method.output_ty,
201                );
202
203                method_calls = quote! {
204                    $method_calls
205
206                    fn $(&object.name)_$(&method.name)(payload: Vec<u8>) -> u32 {
207                        async fn method_call(input: $input_type_tokens) -> $output_type_tokens {
208                            STATE.hooks().$(&object.name).clone().$(&method.name)(input).await
209                        }
210
211                        modrpc_wasm_glue::method_call(
212                            method_call,
213                            host::notify_$(&object.name)_$(&method.name),
214                            payload,
215                        )
216                    }
217                };
218            }
219        }
220    }
221
222    ////////////////////////////////////
223
224    let interface_prefix = &format!("{}::{}", interface_crate_name, interface.name);
225    let interface_role_name = &format!("{}{}", interface.name, role_name);
226    let interface_role_prefix = &format!("{}::{}", interface_crate_name, interface_role_name);
227    let interface_role_name_wit = camel_to_kebab_case(&format!("{}{}", interface.name, role_name));
228
229    quote! {
230        use std::cell::RefCell;
231
232        wit_bindgen::generate!({
233            world: $("\"")$(interface_role_name_wit)$("\""),
234            path: "../wit/",
235        });
236
237        struct State {
238            hooks: RefCell<Option<$(interface_role_prefix)Hooks>>,
239        }
240
241        impl State {
242            fn hooks(&'_ self) -> impl std::ops::Deref<Target = $(interface_role_prefix)Hooks> + '_ {
243                std::cell::Ref::map(self.hooks.borrow(), |x| x.as_ref().unwrap())
244            }
245        }
246
247        #[thread_local]
248        static STATE: State = State {
249            hooks: RefCell::new(None),
250        };
251
252        struct RoleImpl;
253
254        impl main::Main for RoleImpl {
255            fn handle_packet(buf: Vec<u8>) {
256                modrpc_wasm_glue::receive_packet_bundle(buf);
257            }
258
259            fn init(service_id: u32, endpoint_addr: u64, init_state_buf: Vec<u8>) {
260                let init_state: $(interface_prefix)InitState =
261                    // TODO no unwrap?
262                    mproto::decode_value(&init_state_buf).unwrap();
263
264                let hooks = modrpc_wasm_glue::init::<$(interface_role_prefix)Role>(
265                    service_id, endpoint_addr,
266                    init_state,
267                    host::modrpc_send_packet_bundle,
268                    start,
269                );
270                *STATE.hooks.borrow_mut() = Some(hooks);
271            }
272
273            fn tick() {
274                modrpc_wasm_glue::tick();
275            }
276        }
277
278        impl guest::Guest for RoleImpl {
279            $method_calls
280
281            $required_impl_notifies
282        }
283
284        fn start(
285            s: &mut modrpc::RoleSetup,
286            stubs: $(interface_role_prefix)Stubs,
287            hooks: &$(interface_role_prefix)Hooks,
288        ) {
289            $object_builds
290        }
291
292        export_$(camel_to_snake_case(interface_role_name))!(RoleImpl);
293    }
294}
295
296pub fn js_wit_glue(
297    project_js_package: &str,
298    db: &Database,
299    interface: &Interface,
300    role_name: &str,
301) -> js::Tokens {
302    let mproto_cx = &mproto_codegen::codegen::CodegenCx::new_with_type_params(
303        db.mproto_db(),
304        Some(project_js_package),
305        false,
306        &interface.type_params,
307    );
308
309    ////////////////////////////////////
310    // Objects
311
312    let mut object_hooks_interface_fields: js::Tokens = quote! {};
313    let mut object_impls_interface_fields: js::Tokens = quote! {};
314    let mut required_impls: js::Tokens = quote! {};
315    let mut method_resolvers: js::Tokens = quote! {};
316    let mut method_notifies: js::Tokens = quote! {};
317    let mut object_hooks: js::Tokens = quote! {};
318
319    for object in &interface.objects {
320        let Some(object_interface) = db.lookup_interface(&object.construct) else {
321            panic!("failed to lookup interface '{}'", object.construct.name);
322        };
323
324        let Some(object_role_name) = object.get_role_for_parent_role(object_interface, role_name)
325        else {
326            panic!(
327                "object '{}' failed to find role arg '{}'",
328                object.name, role_name
329            );
330        };
331        let object_interface_role_name = &format!("{}{}", object.construct.name, object_role_name);
332
333        let object_mproto_cx =
334            &mproto_cx.with_type_args(&object_interface.type_params, &object.type_args);
335
336        for required_impls_list in &object_interface.required_impls {
337            if !required_impls_list.roles.contains(object_role_name) {
338                continue;
339            }
340
341            for required_impl in &required_impls_list.required_impls {
342                let input_type = mproto_codegen::codegen::js::js_type_tokens(
343                    object_mproto_cx,
344                    &required_impl.input_ty,
345                );
346                let output_type = mproto_codegen::codegen::js::js_type_tokens(
347                    object_mproto_cx,
348                    &required_impl.output_ty,
349                );
350
351                let input_encoder = mproto_codegen::codegen::js::js_type_encoder(
352                    object_mproto_cx,
353                    &required_impl.input_ty,
354                );
355                let output_encoder = mproto_codegen::codegen::js::js_type_encoder(
356                    object_mproto_cx,
357                    &required_impl.output_ty,
358                );
359
360                let impl_name_camel =
361                    &snake_to_camel_case(&format!("{}_{}", object.name, required_impl.name));
362                let impl_name_upper_camel =
363                    &snake_to_upper_camel_case(&format!("{}_{}", object.name, required_impl.name));
364
365                required_impls = quote! {
366                    $required_impls
367                    $impl_name_camel(id: number, inputBuf: Uint8Array) {
368                      const input = mproto.decodeValue($input_encoder, inputBuf.buffer);
369                      const result = roleImpl.$impl_name_camel(input);
370                      const outputBuf = new Uint8Array(mproto.encodeValue($output_encoder, result));
371                      wasm.guest.notify$(impl_name_upper_camel)(id, outputBuf);
372                    },
373                };
374
375                object_impls_interface_fields = quote! {
376                    $object_impls_interface_fields
377                    readonly $impl_name_camel: (input: $input_type) => $output_type;
378                };
379            }
380        }
381
382        let mut object_hook_methods: js::Tokens = quote! {};
383        for methods_list in &object_interface.methods {
384            if !methods_list.roles.contains(object_role_name) {
385                continue;
386            }
387
388            for method in &methods_list.methods {
389                let method_name_camel =
390                    &snake_to_camel_case(&format!("{}_{}", object.name, method.name));
391                let method_name_upper_camel =
392                    &snake_to_upper_camel_case(&format!("{}_{}", object.name, method.name));
393
394                if method.is_async {
395                    method_resolvers = quote! {
396                        $method_resolvers
397                        const $(method_name_camel)Resolvers: { [id: number] : (r: any) => void } = { };
398                    };
399
400                    method_notifies = quote! {
401                        $method_notifies
402                        notify$(method_name_upper_camel)(id: number, resultBuf: Uint8Array) {
403                          $(method_name_camel)Resolvers[id](resultBuf);
404                          delete $(method_name_camel)Resolvers[id];
405                        },
406                    };
407
408                    object_hook_methods = quote! {
409                        $object_hook_methods
410                        wasm.guest.$method_name_camel,
411                        $(method_name_camel)Resolvers,
412                    };
413                }
414            }
415        }
416
417        let mut object_hook_type_args: js::Tokens = quote! {};
418        for type_arg in &object.type_args {
419            // Use mproto_cx, not object_mproto_cx since any appearances of type parameters will be
420            // from the interface, not from the object.
421            let encoder = mproto_codegen::codegen::js::js_type_encoder(mproto_cx, type_arg);
422            object_hook_type_args = quote! {
423                $object_hook_type_args
424                $encoder,
425            };
426        }
427
428        if object_interface.methods.len() > 0 {
429            let object_name_camel = &snake_to_camel_case(&object.name);
430
431            let object_class =
432                &mproto_cx.js_import_qualified(&mproto_codegen::ast::QualifiedIdentifier {
433                    name: object_interface_role_name.into(),
434                    module: object.construct.module.clone(),
435                });
436
437            object_hooks_interface_fields = quote! {
438                $object_hooks_interface_fields
439                readonly $object_name_camel: $object_class$(
440                    // Use mproto_cx, not object_mproto_cx since any appearances of type parameters will be
441                    // from the interface, not from the object.
442                    mproto_codegen::codegen::js::js_type_args(
443                        mproto_cx,
444                        &object.type_args,
445                        mproto_codegen::codegen::js::js_type_tokens,
446                    )
447                );
448            };
449
450            object_hooks = quote! {
451                $object_hooks
452                $object_name_camel: new $object_class(
453                  $object_hook_type_args
454                  $object_hook_methods
455                ),
456            };
457        }
458    }
459
460    ////////////////////////////////////
461    // index.ts
462
463    let init_state = &js::import(project_js_package, format!("{}InitState", interface.name));
464    let proto_init_state = &js::import(
465        project_js_package,
466        format!("Proto{}InitState", interface.name),
467    );
468
469    let interface_role_name = &format!("{}{}", interface.name, role_name);
470    let interface_role_name_snake = &camel_to_snake_case(interface_role_name);
471
472    quote! {
473        import * as mproto from "mproto";
474        import * as modrpc from "modrpc";
475
476        import { instantiate } from $("\"")../dist/wasm/$(interface_role_name_snake)_modrpc.js$("\"");
477
478        export interface $(interface_role_name)Impl {
479          $object_impls_interface_fields
480        }
481
482        export interface $(interface_role_name)Hooks {
483          readonly modrpcReceivePacketBundle: (buf: Uint8Array) => void;
484          $object_hooks_interface_fields
485        }
486
487        type IRunRole = modrpc.RunInterfaceRole<$init_state, $(interface_role_name)Hooks, $(interface_role_name)Impl>;
488        export const Run$(interface_role_name): IRunRole = new class implements IRunRole {
489          readonly initStateEncoder = $proto_init_state;
490
491          run(
492            serviceId: number,
493            endpointAddr: bigint,
494            sendPacketBundleFn: (buf: Uint8Array) => void,
495            initState: $init_state,
496            roleImpl: $(interface_role_name)Impl,
497          ): Promise<[$(interface_role_name)Hooks, () => void]> {
498            return run$(interface_role_name)(serviceId, endpointAddr, sendPacketBundleFn, initState, roleImpl);
499          }
500        }
501
502        export async function run$(interface_role_name)(
503          serviceId: number,
504          endpointAddr: bigint,
505          sendPacketBundleFn: (buf: Uint8Array) => void,
506          initState: $init_state,
507          roleImpl: $(interface_role_name)Impl,
508        ): Promise<[$(interface_role_name)Hooks, () => void]> {
509          let wasm: any;
510          $method_resolvers
511
512          wasm = await instantiate(fetchCompile, {
513            "host": {
514              modrpcSendPacketBundle: (buf: Uint8Array) => {
515                sendPacketBundleFn(buf);
516              },
517              $required_impls
518              $method_notifies
519            },
520          });
521
522          wasm.main.init(serviceId, endpointAddr, mproto.encodeValue($proto_init_state, initState));
523
524          const hooks = {
525            modrpcReceivePacketBundle: (buf: Uint8Array) => {
526              wasm.main.handlePacket(buf);
527            },
528            $object_hooks
529          };
530
531          let done = false;
532          function stop() {
533            done = true;
534          }
535
536          async function main() {
537            while (!done) {
538              await wasm.main.tick();
539              await sleep(10);
540            }
541          }
542          main();
543
544          return [hooks, stop];
545        }
546
547        const isNode = typeof process !== "undefined" && process.versions && process.versions.node;
548        let _fs: any;
549        async function fetchCompile(url: string) {
550          if (isNode) {
551            _fs = _fs || await import("fs/promises");
552            const relativeUrl = new URL($("`./wasm/${url}`, import.meta.url"));
553            return WebAssembly.compile(await _fs.readFile(relativeUrl));
554          } else {
555            const relativeUrl = new URL($("`./wasm/${url}`, import.meta.url"));
556            return fetch(relativeUrl).then(WebAssembly.compileStreaming);
557          }
558        }
559
560        function sleep(ms: number) {
561          return new Promise(resolve => setTimeout(resolve, ms));
562        }
563    }
564}
565
566fn witify_snake_name(n: &str) -> String {
567    n.replace("_", "-")
568}