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 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 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 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 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 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 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 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}