modrpc_codegen/codegen/wasm/
mod.rs1use 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 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 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 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 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 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 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 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}