Skip to main content

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